fix(remote-control): review-loop hardening for status honesty

- Make the non-live status update an authoritative reset in the main
  process: only portal survives, supportsVolume is forced false, stray
  now-playing fields from callers are dropped (Copilot review)
- Stop Stalker radio status from leaking an unrelated TV channel's EPG:
  the ITV-keyed bulk cache survives itv->radio navigation and Ministra
  ids collide across the two lists, so EPG fields publish for itv only
- Publish the reset snapshot when the M3U active channel clears in
  place (e.g. quitting external MPV), not only on route destroy
- Consider a live external session in the M3U volume gate: a
  diagnostic-recovery MPV/VLC launch owns the audio even while a web
  player is configured; republish capability on session start/end
- Share one REMOTE_CONTROL_RESET_STATUS constant across all four
  integrations instead of four hand-copied literals

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmMT33wgK52QL6JAz468eH
This commit is contained in:
Claude committed 2026-08-09 21:05:05 +00:00
1 parent 4d99326269
commit e4e3a55961
10 files changed
+209 -54

No files matched your search

+21 -13
View File
@@ -80,11 +80,11 @@ Status ingestion from renderer:
- Maintains in-memory `RemoteControlStatus` object returned by `/status`
- Live updates (`isLiveView: true` or unspecified) MERGE into the previous
status, so partial pushes (e.g. the M3U volume-only update) keep the
channel fields. A non-live update (`isLiveView: false`) is treated as a
SNAPSHOT: stale now-playing fields (channel name/number, EPG, volume) are
dropped rather than merged, keeping the remote from advertising a channel
that stopped playing. The snapshot keeps the last known `portal` unless the
update names one.
channel fields. A non-live update (`isLiveView: false`) is an
AUTHORITATIVE RESET: only `portal` survives (the update's value, else the
last known one), `supportsVolume` is forced to `false`, and every other
now-playing field is dropped — even if a caller accidentally includes one.
This keeps the remote from advertising a channel that stopped playing.
### Settings integration
@@ -116,10 +116,15 @@ is treated as unsupported unless it exposes all remote-control methods:
`onRemoteControlCommand`. This keeps PWA/self-hosted builds and partial test
bridges from accidentally activating desktop-only remote-control behavior.
Every integration publishes a reset snapshot
(`{ portal: 'unknown', isLiveView: false, supportsVolume: false }`) in its
Every integration publishes the shared reset snapshot
(`REMOTE_CONTROL_RESET_STATUS` in
`libs/portal/shared/util/src/lib/remote-channel-navigation.ts`:
`{ portal: 'unknown', isLiveView: false, supportsVolume: false }`) in its
`ngOnDestroy`/destroy hook, so leaving a live surface always clears the
remote UI instead of freezing the last channel on it.
remote UI instead of freezing the last channel on it. The M3U player also
publishes it when the active channel is cleared IN PLACE (e.g. quitting an
external MPV/VLC session dispatches `resetActiveChannel` while the route
stays mounted).
## Shared helpers
@@ -164,7 +169,7 @@ Implemented behavior:
- `isLiveView: true`
- channel name/number
- EPG now fields
- `supportsVolume` reflects the EFFECTIVE playback: `true` for built-in inline playback (radio audio, the DASH-forced web player, HTML5/Video.js/ArtPlayer), `false` while MPV/VLC or Embedded MPV owns the audio — the remote UI disables its volume buttons on `false`
- `supportsVolume` reflects the EFFECTIVE playback: `true` for built-in inline playback (radio audio, the DASH-forced web player, HTML5/Video.js/ArtPlayer), `false` while MPV/VLC or Embedded MPV owns the audio — including a diagnostic-recovery "Open in MPV/VLC" launch while a web player remains configured (`isRemoteVolumeSupported` also checks the live external session, and an effect republishes the capability when the session starts or ends). The remote UI disables its volume buttons on `false`
- `volume`, `muted`
- Cleans listeners/subscriptions and publishes the reset snapshot in `ngOnDestroy`.
@@ -211,10 +216,13 @@ Implemented behavior:
- `portal: 'stalker'`
- `isLiveView` for selected content type `itv` OR `radio` with an active
item — the radio route reuses this layout and its remote handlers, so
it reports live status too (channel numbering follows the radio list;
EPG fields stay empty). The index comparison uses
`normalizeStalkerEntityId`, because radio ids (`radio-1`) are not
numeric.
it reports live status too (channel numbering follows the radio list).
EPG fields are published for `itv` ONLY: `selectedItvEpgPrograms` is
fed by the ITV-keyed bulk cache, which survives itv→radio navigation,
and Ministra assigns small integer ids to itv and radio independently
— a radio id routinely collides with an unrelated TV channel. The
index comparison uses `normalizeStalkerEntityId`, because radio ids
(`radio-1`) are not numeric.
- channel name/number + current EPG item
- `supportsVolume: false`
- Cleans listeners and publishes the reset snapshot in `ngOnDestroy`.