Commit graph

4 commits

Author SHA1 Message Date
3fda483688 Make the library page header (title + search bar) fully sticky
Some checks failed
Publier l'image Docker / test (pull_request) Failing after 4s
Publier l'image Docker / publish (pull_request) Has been skipped
The search bar was already sticky on its own; the title above it
still scrolled away. Wraps both in one sticky .libraryHeader block,
scoped to views/library only, and stacks it below the mobile shell's
own sticky top bar instead of racing it for the same spot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 19:20:01 +02:00
939beb6fa9 Add a spinner to every loading state
All checks were successful
Publier l'image Docker / test (push) Successful in 21s
Publier l'image Docker / publish (push) Successful in 7s
Every "Chargement..." spot across the app was plain text with no
visual motion - easy to mistake a still-fetching page for a stuck one.

New .spinner in styles/base.css - a plain CSS ring (border +
border-top-color, no image/icon-font asset), sized and vertically
aligned to drop into an existing text node inline instead of needing
a flex-row restructure. prefers-reduced-motion turns the spin off.

New client/src/loading.ts exports one shared LOADING_HINT constant so
every dynamic call site (Album/Artist/Library/Playlist/Series's own
route entry, Home's per-library carousels, a season switch reloading
its episode list, the player's route-level load) renders identical
markup. Shell.html's sidebar library list and Library.html's #grid
placeholder (both static templates, no JS import) got the same
markup pasted directly. Player.html's #loadingNotice (video
buffering) got the spinner too.

Left alone: a couple of static templates' own inert copies of this
same placeholder text (Playlist.html, Series.html) that are always
synchronously overwritten before ever painting - nothing a viewer
could ever actually see differently. Also left alone: the brief
button-label loading states elsewhere ("Ajout...", "Démarrage...") -
a different, smaller UI element, typically resolved within one REST
round trip.

Verified: full typecheck/build on the client (no server changes at
all). Visually confirmed in a real browser that the spinner renders
correctly inline with text and is genuinely animating.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 23:30:25 +02:00
5be839d2f6 Music bar: refresh vote count on join/leave, progress bar; catalog search
All checks were successful
Publier l'image Docker / test (push) Successful in 23s
Publier l'image Docker / publish (push) Successful in 13s
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>
2026-09-14 16:44:23 +02:00
4a31ee741e Rewrite server + client in TypeScript, service-oriented server architecture
All checks were successful
Publier l'image Docker / publish (push) Successful in 18s
Full rewrite, requested directly. Two independent decisions since
TypeScript can't compile itself away at runtime - "no build step" and
"TypeScript" are mutually exclusive, not a small addition to the existing
setup:
- Server: plain tsc (no bundling need - Node resolves its own requires).
  server/dist mirrors server/src 1:1.
- Client: esbuild (does need bundling) - picked over a heavier tool
  (Vite, webpack) since this app's whole client is a couple thousand
  lines; its built-in CSS-from-JS bundling and a `text` loader for
  importing an HTML template as a string cover everything needed via a
  ~30-line hand-written build script (client/build.mjs), not a config
  file for something built for much bigger apps.
Both projects compile independently (no shared-types package - considered
and rejected as more structural commitment than this app's size
justifies; server/src/types.ts and client/src/types.ts have their own
overlapping DTO copies instead). server/'s `npm run dev` uses tsx for a
compile-free dev loop; `npm run build` (tsc) is what server/Dockerfile's
build stage actually runs. client/'s `npm run watch` is esbuild's own
watch mode.

Server: one singleton service class per feature, generalizing the
PartyManager/MusicQueueService pattern that already existed, rather than
route handlers with logic inline:
- AuthService (from embyAuth.js), CatalogService (NEW - previously ~15
  route handlers' worth of Emby queries/DTO shaping inlined directly in
  index.js), PartyService (renamed from PartyManager), MusicQueueService
  (ported as-is, already service-shaped), SocketGateway (from
  socketHandler.js), EmbyClient (from embyProxy.js - low-level plumbing,
  not called a "service" since it has no state/feature of its own).
- index.ts is now thin wiring only: every route reads the session, calls
  one service method, returns the result.
- Real error-handling pass, not just a port: Express 4 doesn't catch a
  rejected Promise from an async handler on its own - a new asyncRoute()
  wrapper + a single error-handling middleware replace what used to be
  zero handling for a request that outright throws. CatalogService
  methods throw a CatalogFetchError (own HTTP status - getRawItem now
  preserves Emby's real 404 instead of a blanket 502) rather than
  touching res directly.

Client: one folder per view (src/views/<name>/<Name>.ts + .html + .css)
instead of one .js file with the markup inline as a template literal. A
view's static shell moved to its .html (esbuild's text loader); per-item
dynamic content (tile grids, episode/track lists) stays as template
-literal-returning functions in the .ts, same as before. Genuinely shared
CSS (tiles, the presence avatar, the series/album detail-header pattern,
track-list rows) lives under src/styles/, imported by whichever views
need it; src/styles/base.css holds the global primitives, imported once
by app.ts. Deliberately not pushed to zero duplication/renaming shared
classes across views for tidiness alone - not worth the risk in a change
already this large. Player.ts keeps its pre-existing internal shape (the
/party/:id dispatcher + the video renderer in one file, importing
renderMusicPlayer from the sibling folder) - that pattern predates this
rewrite and only needed types, not restructuring.

Two real things the port itself caught, not just carried over:
- A type bug: porting SocketGateway's "members" broadcast, reusing
  PartyService's PublicMember interface would have type-checked fine but
  was the wrong (larger) shape - the socket broadcast never included
  isPaused/positionTicks, only the REST response did. Caught by
  comparing against the original JS line by line, fixed with a dedicated
  SocketMemberDto type.
- A real Express+TypeScript friction point: noUncheckedIndexedAccess
  (enabled in both tsconfigs - genuinely useful elsewhere, e.g.
  PartyService.advanceQueue's array indexing) makes every req.params.xxx
  read a type error, since Express's index-signature typing doesn't know
  a matched route's required :name segment is always present. Resolved
  once with a param(req, name) helper rather than 15 routes' worth of
  scattered assertions.

Verified: both projects type-check cleanly in strict mode (tsc --noEmit -
not just "esbuild didn't complain", which it wouldn't even with type
errors present, since it transpiles without checking). Server: every
unit/integration test from the JS version (party lifecycle logging, the
music queue's timer/vote/pause behavior incl. real setTimeout
auto-advance, the socket integration through SocketGateway+PartyService+
MusicQueueService together, the library-type filter, the playback
-override forcing) re-run against the compiled server/dist output -
identical results; the compiled server was actually started and confirmed
listening cleanly. Client: since esbuild bundles everything into one
app.js (no more individually-importable view modules), testing moved to
driving the real compiled bundle through its actual boot -> login ->
router -> view chain (mocked fetch/Socket.IO/hls.js, real clicks/hash
navigation) - arguably more realistic than the old per-module approach.
Confirmed this way: login, the sidebar, home's carousels, a video party
(presence, host controls, leave), the full music hierarchy, a music party
(presence, vote-skip, pause toggle, the inline browse-and-add panel), and
the series page - all through client/dist's actual output, no thrown
errors anywhere.

Not verified: an actual `docker build` of the new multi-stage Dockerfile -
this dev environment has no Docker at all. The build stage runs the exact
npm run build commands already confirmed working locally, just inside
node:20-alpine - the main remaining risk is something environment
-specific, not the build commands themselves, but a real build is still
the first thing to try after this lands.

docs/architecture.md has the full writeup (new "TypeScript and a
service-oriented architecture" section, plus a corrected top-level
"Components" overview that had gone stale even before this change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 09:38:14 +02:00