Issue fixes:
- #155: /api/browse/playlists/parse now handles YouTube/YouTube Music URLs
- #156: stop passing album MBID to verifyArtistName (was calling MB /artist/{id}
with an album MBID, always 404d); fix spotify trackCount stale value
- #154: remove hardcoded port-3030 detection from getApiBaseUrl -- now returns
relative URLs by default so any host:port mapping works
- #25 (partial): fix spotify playlist trackCount to use tracks.length instead of
stale playlist.tracks.total after pagination
Dead code / quality:
- Remove unused rootFolderPath param from processDownload + call sites
- Remove unused req params in route handlers (prefix _req)
- Remove dead push condition from integration.yml job gate
- Remove dead baseUrl constructor param and private field from ApiService
- Fix LibraryTabs hover effect: remove inline style={{ opacity: 0.1 }} that
overrode Tailwind group-hover; change to group-hover:opacity-10
- Fix mobile tab centering in LibraryTabs (add justify-center)
CI security:
- Mask TEST_PASS before writing to GITHUB_ENV in all three workflow files
- Add missing concurrency block to nightly.yml
- Add username validation + remove credential echo in create-e2e-user.sh
- Fix global.setup.ts error message to mention .env.test
E2E:
- Fix vibe test race condition: replace Promise.race + transient text with
stable trackCount.or(noData) assertion
- Fix security test flakiness: toBe(beforeCount) -> not.toBeGreaterThan for
playlist count check (parallel tests can delete playlists concurrently)
- Fix global.setup.ts error message to reference .env.test file
Vibe map:
- Increase cluster label size (13->15 / 10->12 px) and opacity (50->70 / 35->50)
for slightly better readability
docker-nightly was triggering on every push to main -- a full multi-arch
Docker build on every commit. Moved to schedule-only (4am UTC) + workflow_dispatch.
Added concurrency groups so rapid pushes to main cancel the previous
in-progress integration run rather than stacking.
Each CI run generates a fresh random password with openssl rand, writes it to
GITHUB_ENV, and passes it to both create-e2e-user.sh and Playwright. The test
user is ephemeral (container torn down after the run) but the password is now
unique per run and never committed to source.
Previously KimaE2ETest2026! was hardcoded as a fallback -- an admin account
backdoor anyone with repo access could exploit on a production instance that
had run the setup script.
Remove:
- api-contracts.spec.ts (100% theater -- shape checks with no bug-catching value)
Add auth middleware unit tests (backend/src/middleware/__tests__/auth.test.ts):
- 14 tests covering real code branches: missing header, wrong scheme,
empty token, expired JWT, tokenVersion mismatch (password-change
invalidation), legacy token without tokenVersion claim, deleted user,
requireAdmin 401/403 distinction, requireAuthOrToken query param path
- All tests assert on observable behavior that would fail if specific
lines of production code were removed
Add supertest route integration tests with mocked Prisma:
- auth.route.test.ts: login shape (no passwordHash in response), token
claims verification, no username enumeration (identical 401 for wrong
password and nonexistent user), refresh tokenVersion mismatch, role
injection rejected, admin-only endpoint 403 enforcement
- playlists.route.test.ts: IDOR for GET/PUT/DELETE -- each test asserts
BOTH the 403 status AND that the DB write was not called (catches
ownership check removed or moved after the mutation)
Enhance security.spec.ts (e2e against real Docker stack):
- IDOR: add PUT and DELETE scenarios (not just read)
- All IDOR tests now verify state after failed attempt (re-fetch confirms
resource unchanged/still exists -- not just checking status code)
- Enforce 403 specifically (not [403, 404]) -- 404 masks broken auth checks
- Add img onerror XSS vector alongside script tag vector
- Input validation: verify playlist count unchanged after 400
- Error response: add prisma/ORM internals to leakage checks
Add test infrastructure:
- src/__mocks__/test-env.cjs: sets JWT_SECRET before module load (setupFiles)
- jest.config.js: add setupFiles entry
- supertest + @types/supertest added to backend devDependencies
Adds GitHub Container Registry as a second publish target on release.
Users can now pull from ghcr.io as an alternative to Docker Hub,
avoiding Docker Hub's pull rate limits.
Based on PR #48 by @SupremeMortal, adapted for kima naming.
BullMQ enrichment migration
- Rewrote enrichment pipeline on BullMQ v5: artist, track, podcast workers with
pause/resume/stop support and Bull Board visibility
- Essentia publishes audio:analysis:complete events; CLAP subscribes reactively
instead of polling (eliminates scan delay between phases)
- Thread-safe DB pool in CLAP worker, all DB calls in run_in_executor
- Fixed psycopg2.pool submodule import crash on BullMQ vibe worker startup
- Silenced EnrichmentStateService disconnect error on already-closed Redis conn
Enrichment fixes
- lastfmTags NULL caused mood-tags phase to silently skip all tracks; migration
backfills NULL -> '{}' and sets column default
- Cover art fetch errors for temp-MBID albums (temp-* passed to Cover Art Archive)
- VIBE-VOCAB vocabulary JSON not copied to Docker image (TypeScript omits .json)
- Wired cleanupOldResolved() to run daily; added missing enrichment status indexes
- Fixed circuit breaker reset, orphan cleanup, podcast entityType, hang detection
Soulseek
- Track search-page downloads in activity tracker
- Use async fs.promises.access instead of synchronous existsSync
- Verify file exists on disk before emitting download:complete (#110)
Docker image size: 28.4 GB -> 12.2 GB
- Removed all CUDA/NVIDIA dependencies; switched to CPU-only PyTorch/TensorFlow
- tensorflow-cpu + essentia-tensorflow --no-deps (avoids GPU TF transitive dep)
- Fixed .dockerignore: **/node_modules and **/.next now excluded from build context
PWA / mobile
- Background audio session loss on iOS/Android: SilenceKeepalive singleton,
tryResume() in MediaSession play handler, direct track load on 'ended' event,
visibilitychange/pageshow foreground recovery
- Lock orientation to portrait for Android device lock (#117)
Discovery
- Retry All re-importing albums already in library: apply same three-level filter
as GET /current before creating download jobs; delete stale UnavailableAlbum
records for already-present albums. Closes#34
CI / release
- linux/arm64 added to release and nightly Docker builds (#87)
- Isolated release CI from nightly GHA cache (cache-from/cache-to removed from
docker-publish.yml to guarantee clean release builds)
- Redis vm.overcommit_memory=1 sysctl added to prod and server compose files
Other fixes
- Cross-artist album fallback by title+year prevents library splitting (#50)
- Retry temp-MBID artists after 24h not 7 days; hide temp MBIDs from API (#112)
- 3-attempt ECONNRESET retry on all Deezer getPlaylist call sites (#119)
- check response.ok on health probe — fetch does not throw on 5xx (#104)
- Z-index stacking hierarchy established (MiniPlayer through OverlayPlayer)
- API token display overflow on iPhone (min-w-0/overflow-hidden on flex container)
- Fixed player seek flicker on podcasts (30s skip buttons)
- Added dual-layer seek lock mechanism to prevent stale time updates
- Optimized cached podcast seeking (direct seek before reload fallback)
- Large skips now execute immediately for responsive feel
- Mood mix performance optimizations