` demande une pause") and flags the matching person's circle
(`.avatarPauseRequested`: an orange `#ff9800` border plus a small pause-icon
badge in its corner — `.avatarPauseBadge`, a plain inline two-bar SVG
rather than a Unicode glyph, since pause/play characters default to emoji
presentation on several platforms and look muddy at 10px) — both matched
by the event's `userId` against each circle's `data-user-id`, both clearing
automatically after `PAUSE_REQUEST_HIGHLIGHT_MS` (4s), for every member,
including the requester and the host — deliberately not host-only, so a
request landing while the host isn't looking at the tab is still visible
to everyone else, and the requester gets confirmation their own click
registered. This replaced an earlier plain box-shadow pulse animation and
a separate immediate-local-feedback ring on just the requester's own
circle (`.avatarSent`) — collapsed into one unambiguous visual language
driven purely by the server broadcast (which the requester also receives,
so there's no need for a separate client-only echo).
- `.followerNotice` picked up a `margin: 0` it was missing: it's a ``,
and for an absolutely positioned element `bottom` places the *margin*
edge, so its default ~1em user-agent margin was quietly rendering it 13px
higher than its declared `bottom: 24px` ever suggested. Harmless on its
own until `.pauseRequestBanner` needed to sit a specific gap above it —
found by measuring both rects and seeing the gap read as 2px instead of
the ~15px the two `bottom` values alone implied.
- Nothing about this feature actually pauses anything automatically — "request"
is literal, the host still has to act on it. Adding an actual forced-pause
would be a real design decision (can any member override the host's
playback state? that cuts against "host drives, everyone follows") and
wasn't asked for.
Verified end to end with mocked `members`/`pauseRequested` Socket.IO events
against the real running local server (see session history): the presence
row renders directly under the action bar; initials/colors render correctly
for a two-member party; the host's circle never gets `.avatarClickable`
(neither when viewed by themselves nor by others); a `pauseRequested` event
gives the right circle `rgb(255, 152, 0)` as its computed border color and
`display: flex` on its badge (confirmed via computed styles, not just class
names) while leaving every other circle untouched; rapid repeat clicks on
your own circle within the cooldown collapse to exactly one `requestPause`
emission (three synchronous clicks in one script → the emitted-event count
increased by exactly one — an earlier two-separate-clicks check had looked
like a cooldown failure at first, but that was real wall-clock time between
two separate tool calls exceeding the 5s window, not a code bug); and the
banner/`.followerNotice` gap measured 15px after the margin fix, versus 2px
before it. The host-only controls (`becomeHost`, settings menu,
next-episode button) were re-checked after this change and still work.
## Assumptions to verify
**Confirmed against the real server** (not a guess — actually exercised,
see session history): `POST /Users/AuthenticateByName` really is reachable
from Node's server-side `fetch` with no CORS issue at all (server-to-server
has no CORS concept), returns a real 401 for bad credentials, and the whole
login round-trip (browser → our backend → real Emby → real rejection → our
error message) renders correctly end to end. The `--theme-primary-color-*`
etc. custom properties `client/css/app.css` is built from were read directly
off this exact server's live dark theme, not guessed.
**Not yet exercised against the real server** — a successful login hasn't
happened yet (no real credentials used in testing), so nothing past that
point is confirmed:
- **`/Users/{userId}` for token validation** was dropped — the proxy
redesign no longer needs it at all (sessions are validated once at login
and cached server-side, not re-checked against Emby per-request).
- **Streaming**: the first version hand-built
`/Videos/{id}/master.m3u8?VideoCodec=h264&AudioCodec=aac` directly, and it
broke in the field: Emby's server threw `ArgumentNullException: Value
cannot be null. (Parameter 'key')` — almost certainly an internal
dictionary keyed by something that URL never supplied (`MediaSourceId`,
`PlaySessionId`, `DeviceId` are the usual suspects). Fixed by calling
`GET /Items/{id}/PlaybackInfo?UserId=...` first and using the
`MediaSource`'s own `TranscodingUrl` — the same thing real Emby clients do
— falling back to a hand-built URL (now at least carrying
`MediaSourceId`/`PlaySessionId`/`UserId` from that response) only if no
`TranscodingUrl` comes back. Not yet confirmed against a real response:
whether `TranscodingUrl` is reliably present, and whether it's always HLS
(a direct-play-eligible source might not transcode at all, which `hls.js`
on the client wouldn't handle - see Known limitations).
- **HLS playlist rewriting** (`rewritePlaylist` in `embyProxy.js`) — the
logic is straightforward (rewrite every non-`#` line) but has only been
reasoned about, never run against a playlist Emby actually generated. If
video loads the manifest but segments 404 or hang, start here.
- **Response field names** (`AccessToken`, `User.Id`, `User.Name`,
`ServerId`, `Items[].Id/Name/Overview/ProductionYear/Type`,
`/Users/{id}/Views` returning library folders the same shape as regular
items) — long-standing Emby/Jellyfin convention, not independently
re-confirmed for this Emby version.
- **`X-Emby-Authorization` header format** — same caveat as above.
- **The Emby web client deep-link shape**
(`/web/index.html#!/item?id=X&serverId=Y`) used for "Voir sur Emby" —
matches the long-standing convention, not confirmed against this exact
build's routing.
## UI: Emby-like shell, per-type browsing, episode queue
Reworked to look and navigate more like Emby itself, and to treat each
collection type (movies / TV / music) as its own experience rather than one
generic grid:
- **`client/js/shell.js`** renders the persistent chrome once per login: a
sidebar (collections from `GET /api/catalog/libraries`, one icon per
`collectionType`) + a `#mainContent` element the router (`router.js`)
swaps on every navigation. `router.js` was made idempotent
(`start(root)` can be called again safely) specifically so login → shell
boot can happen more than once in a page's lifetime without piling up
duplicate `hashchange` listeners — the same class of bug the earlier Emby
plugin work ran into with its page lifecycle, worth remembering.
- **`views/home.js`** (route `/home`): a horizontal carousel of every active
party server-wide (`GET /api/parties`, deliberately not filtered to "your"
parties — the point is letting people discover and join one without a
code passed out of band) plus a join-by-code fallback, then one more
carousel per library of its recently-added items
(`GET /api/catalog/libraries/:id/recent`, Emby's own
`/Users/{id}/Items/Latest`) — skipping music (see below).
- **`views/library.js`** (route `/library/:id`): dispatches purely on the
library's `collectionType`. `movies`/`tvshows`/anything else gets a tile
grid (`GET /api/catalog/libraries/:id/items`, `Recursive=false` — Movies
as movies, TV as Series tiles); `music` gets an honest "not built yet"
message instead of a half-working page (see Non-goals).
- **Tiles** (`client/js/tiles.js`, shared by the home carousels and the
library grid so they can't drift apart): poster + name, hover-revealed
overlay with "Lancer une watch party" (only for directly-playable types —
a Series tile gets "Voir les épisodes" instead, linking to `/series/:id`)
and "Voir sur Emby" (opens Emby's own web client via a `embyUrl` the
backend builds from `EMBY_SERVER_URL` + the session's `serverId`).
`:hover`/`:focus-within` reveal it on mouse/keyboard; `@media (hover: none)`
keeps it always visible on touch devices, which have no hover state at all.
- **`views/series.js`** (route `/series/:id`): a detail header — poster,
title + year, overview, a "Voir sur Emby" link — above season tabs
(`GET /api/catalog/series/:id/seasons`, Emby's `/Shows/{id}/Seasons`) and
an episode list per season (`GET /api/catalog/series/:id/episodes`, Emby's
`/Shows/{id}/Episodes?SeasonId=...`), each episode startable as its own
party. The header is just `toItemDto`'s existing fields
(`GET /api/catalog/item/:id`, the same DTO shape as everything else) —
the season/episode browsing itself predates this pass and was already
wired end to end (library grid → Series tile → this route → "Lancer une
watch party" per episode); what this pass added was making the page
actually look like a detail page instead of a bare `