This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
muzick is a self-hosted music player + recommendation engine (deployed at `muzick.kvmx.ru:5174`). Three deployable units — `backend/` (Fastify API), `frontend/` (Vite React SPA), `workers/` (BullMQ job processor) — plus Postgres, Redis, and Typesense. All wired together by `docker-compose.yml`.
## Commands
Each unit is its own npm package; `cd` into it first.
```bash
# backend/ and workers/
npm run dev # tsx watch
npm run typecheck # tsc --noEmit (also runs as prebuild)
npm run build # tsc
# backend/ only
npm test# vitest run
npm run test:watch
npx vitest run src/services/generators.test.ts # single file
npx vitest run -t "comfortGenerator"# single test by name
# frontend/
npm run dev # vite
npm run build # vite build
npm run typecheck
# whole stack
docker-compose up -d --build
```
There is no lint step. `typecheck` is the gate; the `prebuild` hook fails the build on type errors.
## Architecture
**Split by process, sharing one Postgres database.** The backend serves the API and the frontend consumes it; the worker runs offline enrichment/analysis. They communicate only through Postgres (source of truth) and Redis (BullMQ queue). There is no shared code package — `workers/` and `backend/` each carry their own copy of things like `queue.ts` and pg clients.
- **backend/src/app.ts** — the real entry point (`server.ts` just calls `buildApp`). Registers all routes, connects pg/redis/typesense, and runs several `setInterval` background jobs directly in-process: claim-fusion materialized-view refresh (10s), belief decay (hourly), forgotten-profile derivation (nightly). Auth is a single `onRequest` hook keyed on `MUZICK_API_KEY` / `MUZICK_ADMIN_KEY` (both optional — no keys means open); `/api/admin/*` requires the admin key specifically, `/api/health` is always exempt.
- **backend/src/services/** — business logic. `db.service.ts` owns schema application (`ensureSchema`/`runMigrations` run on every boot — the docker init-mount only fires on a fresh volume, so migrations live here). `session-director.service.ts` and `generators.service.ts` implement the recommendation/vibe logic.
- **workers/src/index.ts** — one BullMQ `Worker` with a `switch` on job name (scan_library, metadata refresh, audio analysis, artist similarity, image enrichment…). Also registers cron repeatables (integrity sweep, dislike cleanup, stale-session reaper). External metadata clients live in `workers/src/integrations/`.
- **frontend/src/** — TanStack Router + TanStack Query. `services/api.ts` is the axios base; per-domain service files wrap endpoints. Zustand stores in `store/` hold playback/vibe/toast state. `components/ethos/` is the shared Ethos design-system UI.
### Domain concepts (from README + spec comments)
- **Rolling Vibe** — continuous stream interleaving owned library tracks with "probation" external discoveries. Managed by the session-director.
- **Belief / claim fusion** — enrichment produces `claims` from multiple sources fused into a materialized view; beliefs decay over time. The in-process timers in `app.ts` keep this fresh.
- **Dislike lifecycle** — multi-stage state machine; the worker's cleanup sweep advances it.
## Deployment gotchas (see AGENTS.md)
- **Typesense is pinned to 0.25.1** — do not bump casually, the API breaks across majors.
- **Worker uses `network_mode: host` + a SOCKS5 proxy** (`SOCKS_PROXY_URL`) for all external metadata calls (Last.fm, Discogs, MusicBrainz, etc.).
- **DB schema** is `backend/src/db/schema.sql`, dropped into `docker-entrypoint-initdb.d` — only applied on a fresh volume. Schema changes for existing volumes must go through `db.service.ts` migrations.
- Music dir is a **read-only** bind from `/mnt/hdd1/media/Music` → `/music`.
- Redis host inside compose is `infra-redis` (external `infra-net` network); the host-mode worker reaches it at `127.0.0.1:6379`.
- An older `muzick.service` systemd unit runs the pre-docker backend on port 5213 and may conflict — docker compose is the active deployment.
## Working docs
`PLANS.md`, `AUDIT.md`, `progress.md`, and dated `SESSION-*.md` files track in-flight work and are not part of the running system.
lines); normalize timestamp types; advisory lock around `runMigrations`.
## rewrite only if
Nothing here justifies a rewrite. Every finding is a local repair in structurally sound
code.
The one thing worth reconsidering from first principles is **System A/B (claims → fusion →
beliefs)** — but only *after* findings 3 and 4 are fixed, because that subsystem has never
once been observed running correctly. Judge the design on working behaviour, not on the
current state.
## verification plan
-`docker run` a scratch Postgres with `schema.sql`, run a real scan, assert
`count(tracks) > 0` — turns finding 1 into a permanent regression test
- add `frontend` to the CI matrix; add a `npm test` job; reinstall frontend `node_modules`;
switch the frontend Dockerfile to `npm ci`
- backend integration tests against a real container for `withTransaction`,
`runMigrations`, `recordPlay`, and the auth hook
- post-fix assertions:
-`claims` duplicate groups → 0
-`max(last_decayed_at)` advances hourly
- zero EROFS errors in 24h
- Jobs page renders stats
## open decisions
**The dislike lifecycle.** The spec says delete the file; the mount is read-only. Pick one:
- **(a) Drop hard deletion.** Terminal state becomes `HIDDEN`; amend invariant §C; delete
`finalizeDeleted`. Safe, honest, and the library stays immutable. **Recommended** — the
library is a read-only bind for a reason.
- **(b) Make deletion real.** Requires an `rw` mount. `cleanup.service.ts` currently unlinks
*before* its DB transaction and has no dry-run and no per-sweep cap; that ordering must be
fixed first.
Either way, clear the 37 stuck `WARNED` rows — they generate ~300 errors/day.
## uncertainties
- No HTTP endpoint could be exercised: the review sandbox blocked egress (identical 503s
that never reached the backend, confirmed absent from its logs). The auth and SSRF
findings are established from configuration and code, not from a live request. The SSRF
one is unambiguous in code.
-`docker-compose.yml` sets **no resource limits**, though
`docs/architecture/02-invariants-and-risks.md` §B names cgroup limits as the mitigation
for Essentia starving the API. Unquantified in practice.
- Whether the `discovery` / `contextual` profiles are empty because decay never ran or
because nothing ever wrote them is not yet separable; re-evaluate after finding 3 is fixed.
---
## second-opinion review, 2026-07-30 (Claude)
Independently spot-checked the load-bearing findings against the code. **Verdict endorsed:
repair, do not rewrite.**
Verified directly:
- **Finding 1** — `schema.sql:50` declares `canonical_name TEXT NOT NULL` with no default;
`scanner.service.ts:241` inserts only `(name)`. Confirmed.
- **Finding 3** — the `decayBeliefs` CTE is `WITH halflives AS (SELECT profile, CASE ...)`
with no `FROM` clause; Postgres rejects it on every hourly run. Confirmed; the
`VALUES`-based rewrite is the right fix.
- **Finding 4** — `UNIQUE (..., source, user_id)` with nullable `user_id` means
`ON CONFLICT` never fires for objective claims. Confirmed; `NULLS NOT DISTINCT` (PG16)
is correct, and dedup must indeed precede the constraint.
- **Finding 5** — `updateTrack` interpolates `Object.keys(body)` into the SET clause.
Confirmed.
- **Finding 7** — `next()` does `queue.slice(idx + 1)`, so the current track is always
index 0; `prev()`'s `idx > 0` guard can never pass after an auto-advance, and repeat-all
restarts from the track that just ended. Confirmed. The uncommitted end-of-queue fix in
the working tree is correct and independent of this bug.
- **Finding 2** — `nginx.conf.template` injects only `Bearer ${MUZICK_API_KEY}` for all of
`/api`, no admin-key location block. Confirmed at the config layer.
Judgment calls also endorsed: the repair ordering (rebuildability → admin UI → learning
loop), the "corrected claims" discipline, and option (a) for the dislike lifecycle — the
`:ro` mount is intentional.
Two additions:
1.**Finding 3 rollout:** the first successful decay run applies ~23 days of accrued decay
at once (obsession beliefs → ~0.32×). Correct behaviour, but recommendations will shift
visibly — expected, not a regression.
2.**Finding 4 dedup:** the one-time migration should collapse each duplicate group keeping
`MAX(last_reinforced_at)` (and ideally max confidence), not merely delete extra rows,
or reinforcement recency is lost.
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.