Two small, unrelated requests handled together.
The search bar (tiles.ts's shared .searchBar, used by views/library/
artist/album) scrolled away with the page - asked to stay reachable
while scrolling a long grid. position: sticky; top: 0 on the input
itself; base.css's own generic input rule already gives it an opaque
background, which is what keeps tiles scrolling underneath from
showing through once it's stuck - nothing else needed changing.
Seeking within the current music track was explicit, deliberate scope
this session's own "participative playlist" work had cut - the
progress bar was marked read-only specifically because MusicQueueService
had no seek concept, just play/pause/advance. The permission model was
already decided by that class's own documented reasoning for why
*anyone* can pause/resume (reversible, doesn't change what's playing) -
a seek is exactly that same shape, continuous instead of binary, so it
gets the same treatment, not voteSkip's majority gate.
MusicQueueService.seek mirrors setPaused's own elapsed-time math
(clamped into [0, durationSeconds]), with one real difference: setPaused's
resume branch gets away with calling scheduleAdvance directly only
because pausing already cleared the timer first - scheduleAdvance
itself never clears a prior one. Seeking while still playing has no
such guarantee, so skipping that clear first would leave the OLD timer
(counting down against the old position) running alongside the new
one, eventually firing playNext at the wrong moment. New musicSeek
socket event, no host check (same as musicPlaystate). Client-side,
MusicBar's progress track gained the same pointerdown/move/up
drag-to-seek handlers the video player's seek bar already has - no host
branch to port this time, since a listen party has none - plus a
local, optimistic activeDriver.currentTime write before broadcasting,
for the same instant-feedback reason the video player's own
seekFromPointerEvent does that.
Verified: full typecheck/test/build on both projects (128/128 server
tests - new coverage for seek's clamping, its paused-vs-playing
branches, and a real-timer test proving the old advance timer is
cleared rather than left running alongside a re-armed one - the exact
bug the naive "just call scheduleAdvance again" version would have
shipped). No server needed for the sticky search bar - checked by
reading .mainContent's own layout directly (no nested scroll container
between it and the document, confirming position: sticky resolves
against the page scroll as intended).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three separate reports handled together.
Cast was video-only by design (docs/architecture.md flagged it as
video-parties-only for v1) - asked for directly, and turned out to need
almost nothing new server-side: CastTokenService and the 4 streaming
routes' sessionForStream fallback were already keyed on nothing more
specific than partyId. cast.ts's localDriver only needed its parameter
widened from HTMLVideoElement to HTMLMediaElement - both already share
the same currentTime/paused/duration/play()/pause() surface. MusicBar.ts
gained the same activeDriver indirection Player.ts already has, with one
real difference from the video case: a listen party's queue advances
far more often than a video party changes items, so dropping the cast
on every track (the video path's own choice there) would have made this
worse than not having it - a track change while casting instead calls
startCast() again, reusing the already-connected session (a
cache-busting query param added since the content URL itself never
changes between tracks - the server alone decides what's current).
Falls back to local playback if a re-cast throws, same recovery
instinct loadTrack's own hls.js try/catch already has.
The queue popup: reported separately, a fixed 480px width and a header
that scrolled away with the list - both leftover CSS from before the
row shape was reworked, never revisited since. Width cap dropped;
overflow/max-height moved off the outer .musicBarQueue (which wraps the
header too) down onto #queueList alone.
The catalog: still too slow, the front freezing while it loads.
CatalogService.getLibraryItems was asking Emby for Overview on every
one of up to 300 items and never using it (views/library's tiles only
ever show a poster/name/year) - dropped. getArtists had no Limit at
all, unlike every sibling query. Separately, tiles.ts's shared render
path built one HTML string for a whole batch and inserted it via a
single innerHTML write - fine for a 16-item carousel, a real hitch for
a 300-tile grid. renderTilesInChunks inserts the same tileHtml output
40 at a time across requestAnimationFrame callbacks instead;
wireTileButtons switched to one delegated listener so a tile from a
later chunk is still clickable.
Verified: full typecheck/test/build on both projects (122/122 server
tests). The chunked-render mechanism was verified directly - a
synthetic 300-item list against a real DOM confirms every item lands
and a button in the last chunk still fires through the delegated
listener. Not verified: no real Chromecast or Emby login reachable from
this sandbox, so the actual on-TV re-cast round trip and the
before/after catalog latency against a real large library are both
unconfirmed - same standing gap this project already flags for its
other casting work.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Four smaller fixes from real usage:
- MusicBar.ts: "votes needed" for skip depends on the connected-member
count (renderMembers/"members") AND the vote tally (applyMusicState/
"queueUpdated") - the label was only ever recomputed from the latter,
so a plain join/leave with no vote left it stale. Cached the last
MusicPublicState and factored the label math into its own
updateVoteSkipLabel(), called from both handlers now.
- Home.ts: the listen party this tab is already in still showed
"Rejoindre" on the home page. Checks each card's partyId against
musicBar/MusicBar.ts's own getActiveMusicPartyId() (already tracked
client-side) and swaps the button for an inert "En écoute" label on
that one card. No equivalent case for video - its player is a
full-screen takeover, so you can't be looking at this list from inside
one.
- MusicBar.html/.css/.ts: a read-only progress bar (a thin strip along
the bar's own top edge). Deliberately not seekable - no host to
arbitrate a jump in this model, and MusicQueueService has no "seek"
concept server-side today; that would be a real protocol addition, out
of scope here. Fill width is just currentTime/duration, ticked by the
same interval that already updates the position text.
- tiles.ts (new wireSearchFilter), views/library, views/artist,
views/album: a search input per catalog page, filtering whatever's
already rendered client-side (a library page tops out at a few hundred
items - well within "just filter the DOM" territory, no server round
trip per keystroke). Shared helper so the three pages behave
identically. The existing /api/catalog/search endpoint (video types
only, never wired into any UI) is a separate, unrelated feature - a
global cross-library search - left as-is.
Verified against the real compiled bundle: a "members" push alone
updates the skip label; the home page's own party shows "En écoute" while
others still show "Rejoindre"; the progress fill tracks a simulated
currentTime correctly; typing into a library's search box filters tiles
live with the empty-state notice appearing only when nothing matches.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix: player settings menu stuck permanently open
----------------------------------------------
.hide { display: none; } and Player.css's .settingsPanel { display: flex;
... } are both single-class selectors - equal specificity - so whichever
one esbuild happened to bundle later into app.css won the `display`
property on an element carrying both classes. Player.css's own
display:flex always landed after base.css's .hide, so #settingsPanel
showed regardless of the hide class ever being toggled - the toggle logic
itself was correct the whole time, it just had no visible effect.
Fixed at the utility class, not the one call site: `.hide` now uses
`display: none !important`, which wins a same-specificity tie regardless
of bundling order - closes off the whole bug class, since several other
components already set their own `display` for a visible state.
Feature: music player is now a persistent bottom bar, not a page
------------------------------------------------------------------
The music player was its own route (/party/:id -> views/musicPlayer/),
which meant navigating anywhere else - the sidebar, an album to queue up
more tracks - tore the whole thing down (router.ts calls a route's
cleanup, which used to call leaveParty, on every hash change). No way to
browse and listen at the same time.
- client/src/musicBar/MusicBar.ts (new): a Shell-level singleton, mounted
once into a #musicBar element that's a sibling of #mainContent, never
touched by route changes. Renders as a fixed bottom bar. Exposes
initMusicBar/joinMusicParty/leaveMusicParty instead of the router's
render()-returns-cleanup pattern, since it never registers as a route.
- views/player/Player.ts's dispatcher: a music party no longer renders a
page - it calls joinMusicParty() and bounces back to /home (a no-op for
every reachable call site except album's). A video party now calls
leaveMusicParty() first (harmless no-op if nothing's active) - starting
or joining a video watch party always leaves the listen party, since
every party-join call site already funnels through this one dispatcher.
- views/album/Album.ts: the last per-track "start a party" UI now that
the music player's own inline browse panel is gone (browsing just
means navigating there normally). Each track's button starts a new
party or adds to whichever one is already active, decided by
getActiveMusicPartyId() at CLICK time (not render time) so the action
taken is always correct even if the label is momentarily stale; every
button is relabeled in place after a successful click. Starting a
party from here no longer navigates away at all.
- views/musicPlayer/ deleted entirely (MusicPlayer.ts/.html/.css) -
superseded by musicBar/.
- Comment updates in global.d.ts/presence.css/trackList.css/party.ts/
types.ts that named the old module by path.
Verified both fixes by rebuilding and driving the real compiled bundle
through a local static server, with fetch/window.io/window.Hls mocked:
starting a party from an album track leaves the sidebar/page in place and
relabels the other tracks; adding a track posts to the already-active
party's id; navigating into a video party hides the bar, disconnects its
socket, and posts a real leave for the music party before the video
player renders; the settings gear opens/closes correctly for the host in
that same run.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>