feat(vps): port music albums/tracks/playlists CRUD (Step 6c)#191
Merged
guidefari merged 2 commits intoJul 12, 2026
Merged
Conversation
…group (Step 6c) Fixes the /api/music/albums (and tracks/playlists) 404 regression from d052ce8, which deleted routes/music/* claiming it was dead code already superseded -- it wasn't. This breaks apps/www admin UI's Albums/Tracks tabs (useAdminAlbums/useAdminTracks in routes/admin/music.tsx). Ports: album/track/playlist CRUD, playlist-tracks management, spotify track-add/playlist-import/sync-links. Entity-links, resolve, scrape, and the pending-links review queue are deliberately out of scope for this PR (separate stacked PR to follow) -- they're a distinct concern (link review workflow vs entity CRUD) with their own real apps/www consumers. Follows the same convention as the already-merged artist group in this file: AuthMiddleware only requires login, admin-role enforcement happens in-handler via requireAdmin so it can return Forbidden without baking a hard admin gate into the schema layer. Backed entirely by the existing, already-wired MusicEntityService -- no service-layer changes needed, this is pure route-layer work.
…aylist port - addSpotifyTrackToPlaylist now maps a bad-URL SpotifyError (statusCode 400) to HttpApiError.BadRequest instead of dying on it as a 500 -- matches the sibling importSpotifyPlaylist handler's existing behavior for the same failure mode. - CreateAlbumInput/CreateTrackInput/CreatePlaylistInput now reject an empty title/slug (NonEmptyString), matching the old Zod schemas' .min(1) -- a plain Schema.String let '' through to a NOT NULL varchar column. - UpdateAlbumInput/UpdateTrackInput/UpdatePlaylistInput now accept null (not just absent) on every optional field -- the admin edit forms submit full form state on every save, so an unset field arrives as null. Schema.optional alone rejected that, breaking every edit of an album/track with any unset field. - AddSpotifyTrackToPlaylistInput/ImportSpotifyPlaylistInput.url now validate as a URL, matching the old Zod schemas' .url(). - packages/api/src/music.test.ts pins the two schema-level fixes directly so they can't silently regress.
Owner
Author
|
Addressed 3 findings from adversarial review (commit ff48fa0):
Full blackbox suite (102/102) and the new |
5 tasks
guidefari
added a commit
that referenced
this pull request
Jul 12, 2026
… 6d) Completes the music album/track/playlist port (#191) with the remaining groups: entity-link CRUD (list/add/update-status/delete), resolveMusicEntity (paste-a-URL flow), scrapeEntityLinks (trigger link discovery), and listPendingLinks (admin review queue). Real apps/www consumers: useAdminEntityLinks/useAddAdminEntityLink/ useUpdateAdminEntityLinkStatus/useDeleteAdminEntityLink (used in both -MusicEntityDetailPage.tsx and -TweetCapturePage.tsx) and useResolveMusicEntity (-TweetCapturePage.tsx's paste-a-link flow). listEntityLinks is intentionally public (no AuthMiddleware), matching the old Hono route -- everything else (add/update-status/delete/resolve/ scrape/pending-queue) requires admin via the same requireAdmin in-handler check used throughout this file. resolveMusicEntity ports the cover-image-copy side effect from the old handler verbatim: on a successful scrape with a coverImageUrl, best-effort fetch+reupload to our own S3 bucket so entity images don't depend on a third-party URL's long-term availability; a failed copy falls back to the original URL rather than failing the whole request.
5 tasks
guidefari
added a commit
that referenced
this pull request
Jul 12, 2026
…record #191/#193 lessons - migration.md: mark the previously-flagged music-group follow-up resolved (#191, #193), no longer tracked as outstanding. - process.md: record two recurring bug patterns from adversarial review on both PRs (Zod-constraint-not-carried-forward, Update*Input null-rejection), and the PR-auto-close-on-deleted-base-branch gotcha hit when #191 merged while #192 was still open.
github-actions Bot
pushed a commit
that referenced
this pull request
Jul 18, 2026
# [2.66.0](v2.65.4...v2.66.0) (2026-07-18) ### Bug Fixes * **email:** tighten input validation ([ca45177](ca45177)) * password reset flow - when passwords arent matching ([89fb3fa](89fb3fa)) * **upload:** partNumber must decode from a string in multipart bodies ([1b19994](1b19994)), closes [#187](#187) * **vps:** add missing packages/api copy to Dockerfile ([9ad0050](9ad0050)) * **vps:** address adversarial review findings on music album/track/playlist port ([ff48fa0](ff48fa0)) * **vps:** address adversarial review findings on music entity-links port ([4191370](4191370)) * **vps:** auto-default OTel endpoint to localhost in dev stages ([acbf6ea](acbf6ea)) * **vps:** avatar upload 400 from Schema.File multipart mismatch ([6a8d78c](6a8d78c)) * **vps:** complete resolve HttpApi cutover (Step 6) ([88921f5](88921f5)), closes [#161](#161) * **vps:** confirmInvite as a real HttpApiEndpoint, not a raw route ([8f326b0](8f326b0)) * **vps:** correct music/albums gap attribution, document middleware ordering ([76704a6](76704a6)), closes [#189](#189) * **vps:** remove no-op type assertion flagged by oxlint ([0c402d0](0c402d0)), closes [#151](#151) * **vps:** restore email/UUID format validation and await on newsletter unsubscribe ([7157dfe](7157dfe)) * **vps:** restore non-empty-string validation on file-manager fields ([36983bc](36983bc)), closes [#165](#165) [#166](#166) * **vps:** restore UUID format validation on favorites audioId/showId ([72d40fc](72d40fc)) * **vps:** restore UUID validation on shows subscribe/unsubscribe id param ([a5c19d0](a5c19d0)) * **vps:** tighten email validation to match zod's z.email() ([bf6b12f](bf6b12f)), closes [#170](#170) * **vps:** use real URL parsing for spotify enrich endpoint ([b2dbadb](b2dbadb)) * **www:** convert Date fields before sending artist metadata updates ([df07f39](df07f39)), closes [#184](#184) * **www:** make 'Resend email' link visibly clickable ([77cdbe1](77cdbe1)) * **www:** wire SocialLinksCard into the actual profile route ([8a7eb11](8a7eb11)) ### Features * **api:** scaffold packages/api with health group contract ([c9b5235](c9b5235)) * **email:** port email vertical slice ([36913ab](36913ab)) * **vps:** add unused Effect toWebHandler + Hono fallback (Step 2a) ([9899df5](9899df5)) * **vps:** dual-export traces to motel alongside Jaeger ([3a59df8](3a59df8)) * **vps:** move CORS/rate-limit/logging/Sentry to Effect global middleware, remove HonoFallback (Step 8) ([cfb13e8](cfb13e8)) * **vps:** port admin to HttpApiBuilder.group (Step 6) ([b0ec807](b0ec807)) * **vps:** port audio group to HttpApiBuilder (step 6) ([0539002](0539002)) * **vps:** port auth middleware to HttpApiMiddleware (Step 3b) ([28ffae7](28ffae7)) * **vps:** port better-auth route onto the Effect router (Step 2c) ([300b58c](300b58c)) * **vps:** port favorites to HttpApiBuilder.group (Step 6) ([e82ef4f](e82ef4f)) * **vps:** port file-manager to HttpApiBuilder.group (Step 6) ([54ba078](54ba078)) * **vps:** port health to HttpApiBuilder.group (Step 3a) ([bca15fb](bca15fb)) * **vps:** port invite to HttpApiBuilder.group (Step 6) ([65a091a](65a091a)) * **vps:** port label group to HttpApiBuilder (step 6) ([d438195](d438195)) * **vps:** port music albums/tracks/playlists CRUD to HttpApiBuilder.group (Step 6c) ([40873cf](40873cf)) * **vps:** port music entity-links/resolve/scrape/pending-queue (Step 6d) ([7d9ad0c](7d9ad0c)), closes [#191](#191) * **vps:** port music-artists CRUD to HttpApiBuilder.group (Step 4) ([18dafca](18dafca)) * **vps:** port music-reminders to HttpApiBuilder.group (Step 6) ([5cc0c45](5cc0c45)) * **vps:** port newsletter to HttpApiBuilder.group (Step 6) ([eb1a2a6](eb1a2a6)) * **vps:** port post group to HttpApiBuilder (step 6) ([374a480](374a480)) * **vps:** port profile to HttpApiBuilder.group (Step 6) ([095ab24](095ab24)) * **vps:** port release group to HttpApiBuilder (step 6) ([1db4dd8](1db4dd8)) * **vps:** port rss/seo/share routes to plain HttpRouter (Step 7) ([24e57e9](24e57e9)) * **vps:** port search to HttpApiBuilder.group (Step 6) ([d052ce8](d052ce8)) * **vps:** port shows to HttpApiBuilder.group (Step 6) ([b8c5d91](b8c5d91)) * **vps:** port spotify to HttpApiBuilder.group (Step 6) ([34ea0a5](34ea0a5)), closes [#165](#165) [#166](#166) [#167](#167) * **vps:** port upload + upload-multipart to HttpApiBuilder.group (Step 7) ([e3b78d4](e3b78d4)), closes [175/#184](#184) * **vps:** port user group to HttpApiBuilder (step 6) ([0fe5c29](0fe5c29)) * **vps:** swap entry point to Effect toWebHandler (Step 2b) ([5454eea](5454eea)) * **vps:** wire newsletter group into routes.ts/api.ts, delete old Hono trio ([98f8d47](98f8d47)) * **vps:** wire OtlpLive into AppLayer to export traces ([732e625](732e625)) * **www:** add music reminder deletion ([92d6dbb](92d6dbb)) * **www:** add typed HttpApiClient for the music-artists group ([7aa6dd0](7aa6dd0)) * **www:** improve auth navigation ([4a70fd6](4a70fd6)) * **www:** replace useAdminArtists' fetcher with the typed client ([90b6622](90b6622)) * **www:** self-service social links editor in dashboard ([ccb2a38](ccb2a38)) * **www:** swap admin-group hooks to typed HttpApiClient (step 6b) ([35bfb5b](35bfb5b)) * **www:** swap audio-group hooks to typed HttpApiClient (step 6b) ([1779b7f](1779b7f)), closes [#176](#176) [#177](#177) [#175](#175) * **www:** swap favorites hooks to typed HttpApiClient (step 6b) ([c98bc94](c98bc94)) * **www:** swap label+release hooks to typed HttpApiClient (step 6b) ([150031a](150031a)), closes [171/#172](#172) * **www:** swap music-artist hooks to typed HttpApiClient (step 6b) ([fd0cd90](fd0cd90)) * **www:** swap newsletter hooks to typed HttpApiClient (step 6b) ([6843733](6843733)) * **www:** swap newsletter hooks to typed HttpApiClient (step 6b) ([15b4240](15b4240)) * **www:** swap post-group hooks to typed HttpApiClient (step 6b) ([9e86ca7](9e86ca7)), closes [#176](#176) * **www:** swap resolve slug consumer to typed Effect client (step 6b) ([d8ebe7d](d8ebe7d)) * **www:** swap search + profile consumers to typed Effect client (step 6b) ([d1179a8](d1179a8)) * **www:** swap shows-group hooks to typed HttpApiClient (step 6b) ([0c1f9bb](0c1f9bb)) * **www:** swap spotify hooks to typed HttpApiClient (step 6b) ([fb238c7](fb238c7)) * **www:** swap user-group hooks to typed HttpApiClient (step 6b) ([7bce140](7bce140)) * **www:** toast feedback for dashboard profile settings ([11b8edd](11b8edd)) * **www:** warn when reset-password link is opened with an active session ([b71cb61](b71cb61))
|
🎉 This PR is included in version 2.66.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Fixes a live production regression:
/api/music/albums,/api/music/tracks, and/api/music/playlistshave been 404ing since commitd052ce82("port search to HttpApiBuilder.group, Step 6"), which deletedroutes/music/*claiming the code was "dead Hono code... already superseded" byhttp/music.handlers.ts. That claim was false — the new Effect handler only ever implemented artist CRUD + artist-junction endpoints, never albums/tracks/playlists. This has been breakingapps/www's admin UI (useAdminAlbums/useAdminTracksinroutes/admin/music.tsx) since that commit landed.Flagged as tracked follow-up in
docs/migration-effect-http-api.mdafter the Hono-removal PR (#190); this PR resolves it.What
Extends the existing
musicHttpApiGroup(packages/api/src/music.ts) andMusicHandlersLive(apps/vps/src/http/music.handlers.ts) with:runAppFork], sync-links)All backed by the existing, already-wired
MusicEntityService— no service-layer changes, pure route-layer work.Deliberately out of scope (separate stacked PR to follow): entity-links CRUD,
resolveMusicEntity,scrapeEntityLinks,listPendingLinks. These are a distinct concern (link-review workflow vs entity CRUD) with their own realapps/wwwconsumers (useAdminEntityLinks,useResolveMusicEntityin-MusicEntityDetailPage.tsx/-TweetCapturePage.tsx).Admin-role enforcement follows the same convention as the already-merged artist group in this file:
AuthMiddlewareonly requires login;requireAdmininside the handler returnsForbiddenfor non-admins, keeping the schema layer's declared error union honest without a hard admin gate baked into middleware.Test evidence
Blackbox tests (18 new, all passing against a real ephemeral Postgres — 102/102 total in the file):
Live curl verification against dev server with real data:
Response shape matches
apps/www's existingMusicAlbum/MusicTrackinterfaces (apps/www/src/lib/http.ts) field-for-field, so the existinguseAdminAlbums/useAdminTrackshooks (plainfetcher(apiUrl(...)), not the typed client) work against this without anywww-side changes.I attempted a full browser screenshot of the admin UI but couldn't get past the admin-role auth gate without either a known admin credential or writing a role grant to an unconfirmed database — declined to guess/bypass either, so this PR ships with API-contract-level evidence (curl + blackbox tests) rather than a UI screenshot. The UI code itself is unchanged by this PR; only the backend contract it already expects is being restored.
Test plan
bun run typecheckclean (packages/api, apps/vps, apps/www)bun precommitclean (format, lint, typecheck)