The seeded $state initialisers in PlaylistForm intentionally capture the
prop once; untrack() states that instead of tripping
state_referenced_locally. The player now logs the caught exception
rather than discarding it.
It was picked with Math.random() in the component body, so SSR and the
client rendered different text and hydration mismatched. Moving it to
the load keeps the per-visit variety with both sides agreeing, and the
six-level nested ternary becomes a lookup table.
Both pages reimplemented the same accessors over the target union and a
byte-identical queue-item mapper. Moves the domain type (renamed
ContextMenuTarget -> TrackTarget, since tiles and the queue use it too)
and its helpers to $lib/tracks.ts; TrackContextMenu keeps only its menu
geometry. Home drops from 341 to ~230 lines.
Unifies two divergences the duplication was hiding: home compared the
active tile against track.id while the queue is keyed by
queueIdForTrack, so YouTube-sourced tracks never highlighted; and it
hardcoded explicit to false for local tracks, hiding the badge.
Also cancels in-flight youtube filler results when the effect re-runs,
so a slow response cannot overwrite newer ones.
library, playlists and playlists/[id] read locals.user!.sub with no
check, so an anonymous request threw a TypeError and returned 500
instead of redirecting to login. Adds requireUser(), which redirects to
/login?returnTo=... and returns a non-null SessionUser, and drops the
five non-null assertions.
/upload had no load at all: the page rendered for anonymous users and
only failed on submit. It now redirects up front.
It previously appended to the end of the queue; 'Añadir a la cola' is expected to mean the track plays right after the current one, so it now splices in right after the currently playing track's index.
The right-click menu now has a plain 'Añadir a la cola' item (appends the track to player.tracks via the new player.addToQueue) alongside the existing playlist submenu, and its first-level text is text-base to match the rest of the menu.
The player never registered Media Session action handlers, so the OS/keyboard next-track and previous-track keys had nothing to call — only play/pause worked via the browser's built-in audio-element heuristic. Now previoustrack/nexttrack are wired to the player, and metadata/playback state are kept in sync for the lock-screen/OS media widget.
The queue/history identity for a song originating on YouTube now always uses its videoId, instead of switching to the local track id once it finishes downloading. Previously that switch made the same song produce two separate entries when merging session and server listening history.
Create and edit playlist modals now upload/preview a cover image
(reusing the presigned-upload flow already used for tracks) and share
identical layout and coloring. Adds a cover proxy route and wires
PlaylistArt to render a playlist's real cover before falling back to
the track mosaic/gradient.
Also reworks the playlist detail page: full-width borderless "Añadir
de tu biblioteca" table with listens count and click-to-play, drops
the empty-playlist placeholder message, and restyles the playlists
grid to match the rest of the app.
Move authenticated pages under an (app) group with a Sidebar shell, add /auth/login and page-based /login, drop the old explore page and login +server endpoint, and add the server upload helper.
Expand the player store, split player controls (transport, seek, volume, track info), add Cover/PlaylistArt/NowPlaying UI primitives and a cover image proxy route.
- Add data-sveltekit-reload to the /login links so the browser performs a full
navigation (the client router intercepted them and aborted on the external
Zitadel redirect)
- Default post-login returnTo to / (home) since /explore is not built yet
- 'Empezar gratis' now starts the register flow (/login?mode=register)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Configure openid-client with None auth (public client + PKCE) when no secret is
provided, so both PKCE and confidential (Code) Zitadel web apps work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add Authorization Code + PKCE login/logout against Zitadel, handled entirely
server-side with an httpOnly signed session cookie:
- $lib/server/auth: openid-client discovery (config cached), jose-signed
session encode/decode, cookie/name constants
- routes: /login (builds the authorize URL with PKCE/state/nonce, ?mode=register
-> prompt=create), /auth/callback (code exchange + session cookie),
/logout (clears session + Zitadel end_session)
- hooks.server populates locals.user/accessToken from the session
- +layout.server exposes the user; Navbar shows the signed-in user + Salir,
or Iniciar sesión / Registrarse
- switch to adapter-node; env-driven config (.env gitignored, .env.example
committed) with issuer http://host.docker.internal:8080
Verified: svelte-check + build clean; dev smoke test shows the landing in the
logged-out state and /login issuing a 302 to Zitadel's authorize endpoint with
PKCE/state/nonce (discovery reachable). Real client id/secret still needed to
complete the token exchange.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New SvelteKit app in frontend/ (minimal template, TypeScript, TailwindCSS v4).
Adds the app shell (dark music theme navbar + footer) and a landing page with
hero and feature cards. CTAs point to /login and /explore (built in later parts).
Verified: production build succeeds and the page renders with Tailwind styles
applied (checked via preview).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>