- WaveButton: TypeScript port of the prototype's canvas animation
(orb and liquid), breathing at rest, pulsing to the track's BPM and
energy in the cover palette, following the cursor, pressing with a
spring; reduced motion and hidden tabs slow or stop it
- home: the wave in the centre with activity and mood chips; changing
them while playing rebuilds the upcoming tracks without stopping
- player wave mode: asks for the next batch when two tracks are left,
sends complete / skip / early skip / like / dislike as feedback and
refreshes the queue right after a reaction or an early skip
- tracks expose bpm, energy and loudness once analysed; library page
shows analysis progress and 7-day wave metrics; wave style setting
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- /wave/options, POST /wave/sessions (activity, mood, seed),
/next with reactions, PATCH to switch the context on the fly, DELETE
- the recommender is called with an 800 ms budget; on failure the api
serves a fallback wave of likes, favourite artists and random
available tracks with the same dislike / recent / served filters
- every served track is an impression with its source and model; wave
events (context_type wave) set its outcome, reactions win
- library status shows the analysis queue and 7-day wave metrics:
listen-through, early skip and like rate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- core: PlayQueue with shuffle and repeat, PlayAttempt counting only
time actually played, EventBuffer with batching and backoff, uuidv7
- web: Player over HTMLAudioElement with Media Session, preload of the
next track, expired-link refresh, local and server playback state,
hotkeys; PlayerProvider is not wired into the shell yet
- ru/en strings and icons for the player and Collection
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- tracks carry a stream_url signed for the user for 12 h and the
user's reaction; /stream checks the signature, expiry and that the
user is not blocked, then hands the file to nginx by X-Accel-Redirect
(or serves it with Range support when X_ACCEL=false)
- original by default; formats browsers can't play and explicit
high/medium/low go through Opus transcoding, never upwards: a lossy
source under the target bitrate stays original; transcodes are
cached per file and size, deduplicated in flight, limited by
TRANSCODE_CONCURRENCY and trimmed hourly by LRU to the size limit
- /events takes up to 500 events with client UUIDs, drops repeats and
events older than 30 days, counts plays (≥ half or 4 min, or
finished), early skips and history entries (≥ 30 s)
- history with clearing, likes and dislikes with cursors and totals,
track reactions, playback state with last-write-wins, tracks batch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- covers are taken from the embedded picture of the first track or a
cover/folder/front image in the album folder, converted by ffmpeg to
WebP 64/300/1200 and named by content hash, so albums sharing a cover
share the files; a full scan looks again for albums without one
- a 3-colour palette (dominant, second, most saturated accent) from a
deterministic k-means feeds placeholders, page tints and the wave
- /covers/{id} is public but HMAC-signed and immutable; nginx serves
the file via X-Accel-Redirect, X_ACCEL=false serves it from api
- the web client picks the size by display size and pixel density
- install.sh and update.sh generate STREAM_SIGNING_KEY
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Storage admin: local folders with a folder picker and NFS sources,
connection test, scan and full scan with live progress, enable,
disable, delete
- path pattern editor with reorder, examples and a preview table that
shows which pattern matched and what came from tags; compile errors
land under the broken pattern
- Library page with counters, scans and unreadable files; search
reindex for admins
- search box in the top bar: combobox with keyboard navigation, 150 ms
debounce, cancelled stale requests, "/" and Ctrl+K, layout fix hint
- search page with tabs and paging, artist and album pages, album
cards and track lists; unavailable tracks are dimmed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- artists, albums and tracks indexes are rebuilt from PostgreSQL next
to the live ones and swapped in, so search keeps working; a rebuild
runs on start, after every scan and when a source is removed
- transliteration fields (Кино ↔ kino) and a retry in the other
keyboard layout (ghbdtn → привет) reported as corrected_query
- /search/suggest (3 artists, 3 albums, 5 tracks), /search by type
with cursor pages, /admin/search/reindex; Meilisearch is part of
/readyz
- found ids are hydrated from the catalog, so a lagging index never
returns deleted entries
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- schema for sources, path patterns, media files, artists, albums,
tracks, genres (migration 00003)
- path patterns (RE2 with named groups, skip), merge with tags by
source priority, fallbacks from the file name and folder, name
normalization and multi-artist split
- scanner: walk, skip unchanged files by size and mtime, ffprobe in 8
workers with a single catalog writer, renames by content hash keep
the track, duplicates fold into one track with the best file as
primary, missing files are kept 30 days
- storage-agent mounts NFS with validated server, export and an option
whitelist, no shell; test mount counts audio files
- admin API: sources CRUD, connection test, patterns with preview,
scan, library status, file errors, host folder picker; scheduled
scans and health checks; mounts are restored on start
- catalog API: artist, artist albums, album with tracks, track
- api image is alpine with ffmpeg for ffprobe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- core: AuthSession keeps the access token in memory, refreshes it
once for all parallel requests (single-flight) and retries them;
refresh races between tabs are retried, reuse signs out; ApiError
carries problem+json codes and field errors
- web: TanStack Router with guards — guests go to /login, the bootstrap
admin to /setup/admin, temporary passwords to /setup/password, admin
pages by role
- login, setup screens, Classic shell (top bar, role-based sidebar,
empty player), settings: name, theme, language, password, devices
- users admin: search, role filter, create with a one-time temporary
password, reset, block, role change, delete as the policy allows;
audit log for admins
- ru/en strings for auth, admin and error codes; language and theme
are saved in the profile
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- /admin/users: list with search, role filter and cursor pages; create
with a one-time temporary password; rename, change role, block;
reset password; delete
- every change is checked against the role policy, a forbidden field
rejects the whole patch; the last active admin is locked in the
transaction and cannot be demoted, blocked or deleted
- role change and blocking revoke access tokens at once; blocking and
password reset also end all sessions
- /admin/audit: newest-first journal with actor names
- path and query parameter errors are problem+json now
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- users, refresh_tokens and audit_log schema (migration 00002)
- argon2id passwords with rehash on login, password policy, lockout
with exponential back-off and a per-IP rate limit in Redis
- Ed25519 JWT access tokens with kid-based key rotation; rotating
refresh tokens with reuse detection (cookie for web, body for Android)
- bootstrap admin on first start; must_change_credentials and
must_change_password gate every other endpoint
- /auth/login, /auth/refresh, /auth/logout(-all), /me, /me/password,
/me/bootstrap-credentials, /me/sessions; server-info reports
bootstrap_required
- role policy from docs/auth-and-roles.md with table tests
- problem+json errors with field errors; ru/en translations of the codes
- install.sh and update.sh generate JWT_PRIVATE_KEY
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/api/Dockerfile, storage-agent, storage-agent) (push) Canceled after 0s
Security and robustness:
- storage-agent rejects an empty INTERNAL_TOKEN (config notEmpty + middleware)
- api takes the client IP only from X-Real-IP (chi RealIP trusted True-Client-IP)
- /readyz reports ok/fail and logs the cause; checks run in parallel with a timeout
- storage-agent shuts down gracefully; shared httpx.NewServer/Serve with IdleTimeout
- goose migrations take a Postgres advisory lock
- recommender readyz: per-check timeout, redis socket timeouts
Clients:
- Android release allows cleartext HTTP for LAN servers (expo-build-properties)
- checkServer times out after 10s; connect button is disabled while checking
Deploy and CI:
- install.sh hands cache/uploads/updates/models to the non-root service users
- update.sh refuses downgrades unless PIPU_ALLOW_DOWNGRADE=1
- recommender/analyzer get only the env they need; required secrets fail fast
- nginx security headers and CSP; strip True-Client-IP
- dev compose binds ports to 127.0.0.1; postgres/redis images pinned
- version.mjs fails on a malformed explicit tag instead of releasing 0.0.0-dev
- contract check catches untracked generated files
- final releases fail without a release keystore; changelog skips rc tags
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>