Phone remote: status pushes rebuild the list every 2 s, swallowing taps and overwriting typed input #8

Closed
opened 2026-08-26 01:53:46 +02:00 by benjamin · 1 comment
Owner

Problem

renderChannels() runs on every status broadcast — roughly every two seconds while something is playing — and it does three expensive/destructive things:

  1. Full DOM rebuild. list.innerHTML = '' followed by a fresh fragment (index.html:520). If a broadcast lands between touchstart and touchend, the tapped button is removed before the browser can synthesize the click, so the tap is silently dropped. Reproducible any time someone browses channels while playback runs — exactly when it matters.

  2. Overwrites what the user is typing. if (state.playlistUrl) $('playlist-input').value = state.playlistUrl (index.html:522) fires regardless of whether the setup panel is open. Typing a new M3U URL while the TV plays is effectively impossible: keystrokes are replaced every two seconds.

  3. O(n) lookup per row. state.channels.indexOf(c) inside the render loop (index.html:467) scans the whole array for each rendered row. With hundreds of channels that is hundreds of thousands of comparisons every tick, adding jank to the same interaction.

Suggested fix

  • Only re-render the list when the channel data actually changed; update the "currently playing" highlight in place
  • Never write into an input the user has focused
  • Carry the index with the item (forEach((c, i) => …)) instead of indexOf

Found by multi-agent code review; verified against current app/src/main/assets/remote/index.html.

## Problem `renderChannels()` runs on every `status` broadcast — roughly every two seconds while something is playing — and it does three expensive/destructive things: 1. **Full DOM rebuild.** `list.innerHTML = ''` followed by a fresh fragment (`index.html:520`). If a broadcast lands between `touchstart` and `touchend`, the tapped button is removed before the browser can synthesize the `click`, so the tap is silently dropped. Reproducible any time someone browses channels while playback runs — exactly when it matters. 2. **Overwrites what the user is typing.** `if (state.playlistUrl) $('playlist-input').value = state.playlistUrl` (`index.html:522`) fires regardless of whether the setup panel is open. Typing a new M3U URL while the TV plays is effectively impossible: keystrokes are replaced every two seconds. 3. **O(n) lookup per row.** `state.channels.indexOf(c)` inside the render loop (`index.html:467`) scans the whole array for each rendered row. With hundreds of channels that is hundreds of thousands of comparisons every tick, adding jank to the same interaction. ## Suggested fix - Only re-render the list when the channel data actually changed; update the "currently playing" highlight in place - Never write into an input the user has focused - Carry the index with the item (`forEach((c, i) => …)`) instead of `indexOf` Found by multi-agent code review; verified against current `app/src/main/assets/remote/index.html`.
Author
Owner

Fixed in dd612fc, shipped in v0.9.0. All three parts:

  1. No more blind rebuilds. Status pushes go through renderChannelsIfChanged(), which compares a signature (channel count, search term, favourites filter, favourite ids, currently playing channel) and returns early when nothing changed. A tap can no longer be swallowed by a rebuild that happens between touchstart and touchend.
  2. Focused inputs are left alone. The playlist field is only written when it is not document.activeElement, so typing a URL while the TV plays works.
  3. No indexOf in the render loop. Channels carry a _idx assigned once when the list arrives, so each row is O(1) instead of scanning the whole array.

Also picked up along the way: an empty search result now shows "Keine Sender gefunden." (and "Keine Favoriten — Stern auf einem Sender antippen." in the favourites view) instead of rendering a blank list that looks broken.

Fixed in dd612fc, shipped in v0.9.0. All three parts: 1. **No more blind rebuilds.** Status pushes go through `renderChannelsIfChanged()`, which compares a signature (channel count, search term, favourites filter, favourite ids, currently playing channel) and returns early when nothing changed. A tap can no longer be swallowed by a rebuild that happens between touchstart and touchend. 2. **Focused inputs are left alone.** The playlist field is only written when it is not `document.activeElement`, so typing a URL while the TV plays works. 3. **No `indexOf` in the render loop.** Channels carry a `_idx` assigned once when the list arrives, so each row is O(1) instead of scanning the whole array. Also picked up along the way: an empty search result now shows "Keine Sender gefunden." (and "Keine Favoriten — Stern auf einem Sender antippen." in the favourites view) instead of rendering a blank list that looks broken.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: be-nj/castarr#8