audio streaming app plyr.fm
38
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: status maintenance - mood search, genre classification, performance (#882)

- archived January 2026 detailed content to .status_history/2026-01.md
- added missing February PRs: mood search MVP (#848-858), genre classification (#864-872),
GET /tracks/top optimization (#879), portal pagination (#878), repo reorg (#876)
- updated priorities, project structure, cost structure, and what's working sections
- STATUS.md: 611 → 390 lines

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

authored by

claude[bot]
claude[bot]
Claude Opus 4.6
and committed by
GitHub
b3a57234 961d1075

+332 -275
+278
.status_history/2026-01.md
··· 158 158 - **feed/library toggle** (PR #692): consistent header layout with toggle between feed and library views 159 159 - **shuffle button moved** (PR #693): shuffle now in queue component instead of player controls 160 160 - **justfile consistency** (PR #694): standardized `just run` across frontend/backend modules 161 + 162 + --- 163 + 164 + ## Mid-to-Late January 2026 Work (Jan 10-31) 165 + 166 + ### per-track PDS migration + UX polish (PRs #835-839, Jan 30-31) 167 + 168 + **selective migration**: replaced all-or-nothing PDS backfill with a modal where users pick individual tracks to migrate. modal shows file sizes (via R2 HEAD requests), track status badges (on PDS / gated / eligible), and a select-all toggle. 169 + 170 + **non-blocking UX (PR #839)**: the modal initially blocked the user during migration. reworked so the modal is selection-only — picks tracks, fires a callback, closes immediately. POST + SSE progress tracking moved to the parent component with persistent toast updates ("migrating 3/7...", "5 migrated, 2 skipped"). user is never trapped. 171 + 172 + **backend changes (PR #838)**: 173 + - `GET /tracks/me/file-sizes` — parallel R2 HEAD requests (semaphore-capped at 10) to get byte sizes for the migration modal 174 + - `POST /pds-backfill/audio` now accepts optional `track_ids` body to backfill specific tracks (backward-compatible — no body = all eligible) 175 + - SSE progress stream includes `last_processed_track_id` and `last_status` for per-track updates 176 + 177 + **copy fixes (PRs #835-836)**: removed "R2" from user-facing text (settings toggle, upload notes). users see "plyr.fm storage" instead of infrastructure detail. 178 + 179 + **share link clutter (PR #837)**: share links with zero interactions (self-clicks filtered) were cluttering the portal stats section. now hidden until someone else actually clicks the link. 180 + 181 + --- 182 + 183 + ### PDS blob storage for audio (PRs #823-833, Jan 29) 184 + 185 + **audio files can now be stored on the user's PDS** - embraces ATProto's data ownership model. PDS uploads are feature-flagged and opt-in via a user setting, with R2 CDN as the primary delivery path. 186 + 187 + **core implementation (PR #823)**: 188 + - new uploads: audio blob uploaded to PDS, BlobRef stored in track record 189 + - dual-write: R2 copy kept for streaming performance (PDS `getBlob` isn't CDN-optimized) 190 + - graceful fallback: if PDS rejects blob (size limit), track stays R2-only 191 + - gated tracks skip PDS (need auth-protected access) 192 + 193 + **database changes**: 194 + - `audio_storage`: "r2" | "pds" | "both" 195 + - `pds_blob_cid`: CID of blob on user's PDS 196 + - `pds_blob_size`: size in bytes 197 + 198 + **bug fixes and hardening (PRs #824-828)**: 199 + - fix atproto headers lost on DPoP retry (#824) 200 + - fail upload on unexpected PDS errors instead of silent fallback (#825) 201 + - add `blob:*/*` OAuth scope to both permission sets and granular paths (#826, #827) 202 + - remove PDS indicator from track UI — PDS will be the default, no need to badge it (#828) 203 + 204 + **batch backfill (PR #829)**: `POST /pds-backfill/audio` starts a background job (docket) to backfill existing tracks to the user's PDS with SSE progress streaming. frontend `PdsBackfillControl` component in the portal. 205 + 206 + **copyright DM fix (PR #831)**: removed misleading "0% confidence" from copyright notification DMs — the enterprise AudD API doesn't return confidence values. 207 + 208 + **feature flag gating (PR #833)**: PDS uploads during track upload are now gated behind two checks: admin-assigned `pds-audio-uploads` feature flag + per-user toggle in Settings > Experimental. default behavior is R2-only unless both are enabled. 209 + 210 + **terms update (PR #832)**: clarified PDS delisting language in terms of service. 211 + 212 + **research**: documented emerging ATProto media service patterns from [community discourse](https://discourse.atprotocol.community/t/media-pds-service/297) — the ecosystem is converging on dedicated sidecar media services rather than PDS-as-media-host. our layered architecture (R2 + CDN + PDS records) aligns well. see `docs/research/2026-01-29-atproto-media-service-patterns.md`. 213 + 214 + --- 215 + 216 + ### PDS-based account creation (PRs #813-815, Jan 27) 217 + 218 + **create ATProto accounts directly from plyr.fm** - users without an existing ATProto identity can now create one during sign-up by selecting a PDS host. 219 + 220 + **how it works**: 221 + - login page shows "create account" tab when feature is enabled 222 + - user selects a PDS (currently selfhosted.social) 223 + - OAuth flow uses `prompt=create` to trigger account creation on the PDS 224 + - after account creation, user is redirected back and logged in 225 + 226 + **implementation details**: 227 + - `/auth/pds-options` endpoint returns available PDS hosts from config 228 + - `/auth/start` accepts `pds_url` parameter for account creation flow 229 + - handle resolution falls back to PDS directly (via `com.atproto.repo.describeRepo`) when Bluesky AppView hasn't indexed the new account yet 230 + 231 + **configuration** (`AccountCreationSettings`): 232 + - `enabled`: feature flag for account creation 233 + - `recommended_pds`: list of PDS options with name, url, and description 234 + 235 + --- 236 + 237 + ### lossless audio support (PRs #794-801, Jan 25) 238 + 239 + **transcoding integration complete** - users can now upload AIFF and FLAC files. the system transcodes them to MP3 for browser compatibility while preserving originals for lossless playback. 240 + 241 + **how it works**: 242 + - upload AIFF/FLAC → original saved to R2, transcoded MP3 created 243 + - database stores both `file_id` (transcoded) and `original_file_id` (lossless) 244 + - frontend detects browser capabilities via `canPlayType()` 245 + - Safari/native apps get lossless, Chrome/Firefox get transcoded MP3 246 + - lossless badge shown on track cards when browser supports the format 247 + 248 + **key changes**: 249 + - `original_file_id` and `original_file_type` added to Track model and API 250 + - audio endpoint serves either version based on requested file_id 251 + - feature-flagged via `lossless-uploads` user flag 252 + 253 + **bug fixes during rollout**: 254 + - PR #796: audio endpoint now queries by `file_id` OR `original_file_id` 255 + - PR #797: store actual extension (`.aif`) not normalized format name (`.aiff`) 256 + 257 + **UI polish (PRs #799-801)**: 258 + - lossless badge positioned in top-right corner of track card (not artwork) 259 + - subtle glowing animation draws attention to premium quality tracks 260 + - whole card gets accent-colored border treatment when lossless 261 + - theme-aware styling, responsive sizing, respects `prefers-reduced-motion` 262 + 263 + --- 264 + 265 + ### auth check optimization (PRs #781-782, Jan 23) 266 + 267 + **eliminated redundant /auth/me calls** - previously, every navigation triggered an auth check via the layout load function. for unauthenticated users, this meant a 401 on every page click (117 errors in 24 hours observed via Logfire). 268 + 269 + **fix**: auth singleton now tracks initialization state. `+layout.svelte` checks auth once on mount instead of every navigation. follow-up PR fixed library/liked pages that were broken by the layout simplification (they were using `await parent()` to get `isAuthenticated` which was no longer provided). 270 + 271 + --- 272 + 273 + ### remove SSR sensitive-images fetch (PR #785, Jan 24) 274 + 275 + **eliminated unnecessary SSR fetch** - the frontend SSR (`+layout.server.ts`) was fetching `/moderation/sensitive-images` on every page load to pre-populate the client-side moderation filter. during traffic spikes, this hammered the backend (1,179 rate limit hits over 7 days). 276 + 277 + **root cause**: the SSR fetch was premature optimization. cloudflare pages workers make direct fetch calls to fly.io - there's no CDN layer to cache responses. the cache-control headers we added in PR #784 only help browser caching, not SSR-to-origin requests. 278 + 279 + **fix**: removed the SSR fetch entirely. the client-side `ModerationManager` singleton already has caching and will fetch the data once on page load. the "flash of sensitive content" risk is theoretical - images load slower than a single API call completes, and there are only 2 flagged images. 280 + 281 + - deleted `+layout.server.ts` 282 + - simplified `+layout.ts` 283 + - updated pages to use `moderation.isSensitive()` singleton instead of SSR data 284 + 285 + --- 286 + 287 + ### listen receipts (PR #773, Jan 22) 288 + 289 + **share links now track who clicked and played** - when you share a track, you get a URL with a `?ref=` code that records visitors and listeners: 290 + - `POST /tracks/{id}/share` creates tracked share link with unique 8-character code (48 bits entropy) 291 + - frontend captures `?ref=` param on page load, fires click event to backend 292 + - play endpoint accepts optional `ref` param to record play attribution 293 + - `GET /tracks/me/shares` returns paginated stats: visitors, listeners, anonymous counts 294 + 295 + **portal share stats section**: 296 + - expandable cards per share link with copyable tracked URL 297 + - visitors (who clicked) and listeners (who played) shown as avatar circles 298 + - individual interaction counts per user 299 + - self-clicks/plays filtered out to avoid inflating stats 300 + 301 + **data model**: 302 + - `ShareLink` table: code, track_id, creator_did, created_at 303 + - `ShareLinkEvent` table: share_link_id, visitor_did (nullable for anonymous), event_type (click/play) 304 + 305 + --- 306 + 307 + ### handle display fix (PR #774, Jan 22) 308 + 309 + **DIDs were displaying instead of handles** in share link stats and other places (comments, track likers): 310 + - root cause: Artist records were only created during profile setup 311 + - users who authenticated but skipped setup had no Artist record 312 + - fix: create minimal Artist record (did, handle, avatar) during OAuth callback 313 + - profile setup now updates existing record instead of erroring 314 + 315 + --- 316 + 317 + ### responsive embed v2 (PRs #771-772, Jan 20-21) 318 + 319 + **complete rewrite of embed CSS** using container queries and proportional scaling: 320 + 321 + **layout modes**: 322 + - **wide** (width >= 400px): side art, proportional sizing 323 + - **very wide** (width >= 600px): larger art, more breathing room 324 + - **square/tall** (aspect <= 1.2, width >= 200px): art on top, 2-line titles 325 + - **very tall** (aspect <= 0.7, width >= 200px): blurred background overlay 326 + - **narrow** (width < 280px): compact blurred background 327 + - **micro** (width < 200px): hide time labels and logo 328 + 329 + **key technical changes**: 330 + - all sizes use `clamp()` with `cqi` units (container query units) 331 + - grid-based header layout instead of absolute positioning 332 + - gradient overlay (top-heavy to bottom-heavy) for text readability 333 + 334 + --- 335 + 336 + ### terms of service and privacy policy (PRs #567, #761-770, Jan 19-20) 337 + 338 + **legal foundation shipped** with ATProto-aware design: 339 + 340 + **terms cover**: 341 + - AT Protocol context (decentralized identity, user-controlled PDS) 342 + - content ownership (users retain ownership, plyr.fm gets license for streaming) 343 + - DMCA safe harbor with designated agent (DMCA-1069186) 344 + - federation disclaimer: audio files in blob storage we control, but ATProto records may persist on user's PDS 345 + 346 + **privacy policy**: 347 + - explicit third-party list with links (Cloudflare, Fly.io, Neon, Logfire, AudD, Anthropic, ATProtoFans) 348 + - data ownership clarity (DID, profile, tracks on user's PDS) 349 + - MIT license added to repo 350 + 351 + **acceptance flow** (TermsOverlay component): 352 + - shown on first login if `terms_accepted_at` is null 353 + - 4-bullet summary with links to full documents 354 + - "I Accept" or "Decline & Logout" options 355 + - `POST /account/accept-terms` records timestamp 356 + 357 + **polish PRs** (#761-770): corrected ATProto vs "our servers" terminology, standardized AT Protocol naming, added email fallbacks, capitalized sentence starts 358 + 359 + --- 360 + 361 + ### content gating research (Jan 18) 362 + 363 + researched ATProtoFans architecture and JSONLogic rule evaluation. documented findings in `docs/content-gating-roadmap.md`: 364 + - current ATProtoFans records and API (supporter, supporterProof, brokerProof, terms) 365 + - the gap: terms exist but aren't exposed via validateSupporter 366 + - how magazi uses datalogic-rs for flexible rule evaluation 367 + - open questions about upcoming metadata extensions 368 + 369 + no implementation changes - waiting to align with what ATProtoFans will support. 370 + 371 + ### logout modal UX (PRs #755-757, Jan 17-18) 372 + 373 + **tooltip scroll fix** (PR #755): 374 + - leftmost avatar in likers/commenters tooltip was clipped with no way to scroll to it 375 + - changed `justify-content: center` to `flex-start` so most recent (leftmost) is always visible 376 + 377 + **logout modal copy** (PRs #756-757): 378 + - simplified from two confusing questions to one clear question 379 + - before: "stay logged in?" + "you're logging out of @handle?" 380 + - after: "switch accounts?" 381 + - "logout completely" → "log out of all accounts" 382 + 383 + --- 384 + 385 + ### idempotent teal scrobbles (PR #754, Jan 16) 386 + 387 + **prevents duplicate scrobbles** when same play is submitted multiple times: 388 + - use `putRecord` with deterministic TID rkeys derived from `playedTime` instead of `createRecord` 389 + - network retries, multiple teal-compatible services, or background task retries won't create duplicates 390 + - adds `played_time` parameter to `build_teal_play_record` for deterministic record keys 391 + 392 + --- 393 + 394 + ### avatar refresh and tooltip polish (PRs #750-752, Jan 13) 395 + 396 + **avatar refresh from anywhere** (PR #751): 397 + - previously, stale avatar URLs were only fixed when visiting the artist detail page 398 + - now any broken avatar triggers a background refresh from Bluesky 399 + - shared `avatar-refresh.svelte.ts` provides global cache and request deduplication 400 + - works from: track items, likers tooltip, commenters tooltip, profile page 401 + 402 + **interactive tooltips** (PR #750): 403 + - hovering on like count shows avatar circles of users who liked 404 + - hovering on comment count shows avatar circles of commenters 405 + - lazy-loaded with 5-minute cache, invalidated when likes/comments change 406 + - elegant centered layout with horizontal scroll when needed 407 + 408 + **UX polish** (PR #752): 409 + - added prettier config with `useTabs: true` to match existing style 410 + - reduced avatar hover effect intensity (scale 1.2 → 1.08) 411 + - fixed avatar hover clipping at tooltip edge (added top padding) 412 + - track title now links to detail page (color change on hover) 413 + 414 + --- 415 + 416 + ### copyright flagging fix (PR #748, Jan 12) 417 + 418 + **switched from score-based to dominant match detection**: 419 + - AudD's enterprise API doesn't return confidence scores (always 0) 420 + - previous threshold-based detection was broken 421 + - new approach: flag if one song appears in >= 30% of matched segments 422 + - filters false positives where random segments match different songs 423 + 424 + --- 425 + 426 + ### Neon cold start fix (Jan 11) 427 + 428 + **why**: first requests after idle periods would fail with 500 errors due to Neon serverless scaling to zero after 5 minutes of inactivity. previous mitigations (larger pool, longer timeouts) helped but didn't eliminate the problem. 429 + 430 + **fix**: disabled scale-to-zero on `plyr-prd` via Neon console. this is the [recommended approach](https://neon.com/blog/6-best-practices-for-running-neon-in-production) for production workloads. 431 + 432 + **configuration**: 433 + - `plyr-prd`: scale-to-zero **disabled** (`suspend_timeout_seconds: -1`) 434 + - `plyr-stg`, `plyr-dev`: scale-to-zero enabled (cold starts acceptable) 435 + 436 + **docs**: updated [connection-pooling.md](docs/backend/database/connection-pooling.md) with production guidance and how to verify settings via Neon MCP. 437 + 438 + closes #733
+54 -275
STATUS.md
··· 105 105 106 106 --- 107 107 108 - ### January 2026 109 - 110 - #### per-track PDS migration + UX polish (PRs #835-839, Jan 30-31) 111 - 112 - **selective migration**: replaced all-or-nothing PDS backfill with a modal where users pick individual tracks to migrate. modal shows file sizes (via R2 HEAD requests), track status badges (on PDS / gated / eligible), and a select-all toggle. 113 - 114 - **non-blocking UX (PR #839)**: the modal initially blocked the user during migration. reworked so the modal is selection-only — picks tracks, fires a callback, closes immediately. POST + SSE progress tracking moved to the parent component with persistent toast updates ("migrating 3/7...", "5 migrated, 2 skipped"). user is never trapped. 115 - 116 - **backend changes (PR #838)**: 117 - - `GET /tracks/me/file-sizes` — parallel R2 HEAD requests (semaphore-capped at 10) to get byte sizes for the migration modal 118 - - `POST /pds-backfill/audio` now accepts optional `track_ids` body to backfill specific tracks (backward-compatible — no body = all eligible) 119 - - SSE progress stream includes `last_processed_track_id` and `last_status` for per-track updates 120 - 121 - **copy fixes (PRs #835-836)**: removed "R2" from user-facing text (settings toggle, upload notes). users see "plyr.fm storage" instead of infrastructure detail. 122 - 123 - **share link clutter (PR #837)**: share links with zero interactions (self-clicks filtered) were cluttering the portal stats section. now hidden until someone else actually clicks the link. 124 - 125 - --- 126 - 127 - #### PDS blob storage for audio (PRs #823-833, Jan 29) 128 - 129 - **audio files can now be stored on the user's PDS** - embraces ATProto's data ownership model. PDS uploads are feature-flagged and opt-in via a user setting, with R2 CDN as the primary delivery path. 108 + #### mood search MVP + semantic search hardening (PRs #848-858, Feb 5-6) 130 109 131 - **core implementation (PR #823)**: 132 - - new uploads: audio blob uploaded to PDS, BlobRef stored in track record 133 - - dual-write: R2 copy kept for streaming performance (PDS `getBlob` isn't CDN-optimized) 134 - - graceful fallback: if PDS rejects blob (size limit), track stays R2-only 135 - - gated tracks skip PDS (need auth-protected access) 136 - 137 - **database changes**: 138 - - `audio_storage`: "r2" | "pds" | "both" 139 - - `pds_blob_cid`: CID of blob on user's PDS 140 - - `pds_blob_size`: size in bytes 141 - 142 - **bug fixes and hardening (PRs #824-828)**: 143 - - fix atproto headers lost on DPoP retry (#824) 144 - - fail upload on unexpected PDS errors instead of silent fallback (#825) 145 - - add `blob:*/*` OAuth scope to both permission sets and granular paths (#826, #827) 146 - - remove PDS indicator from track UI — PDS will be the default, no need to badge it (#828) 147 - 148 - **batch backfill (PR #829)**: `POST /pds-backfill/audio` starts a background job (docket) to backfill existing tracks to the user's PDS with SSE progress streaming. frontend `PdsBackfillControl` component in the portal. 149 - 150 - **copyright DM fix (PR #831)**: removed misleading "0% confidence" from copyright notification DMs — the enterprise AudD API doesn't return confidence values. 151 - 152 - **feature flag gating (PR #833)**: PDS uploads during track upload are now gated behind two checks: admin-assigned `pds-audio-uploads` feature flag + per-user toggle in Settings > Experimental. default behavior is R2-only unless both are enabled. 153 - 154 - **terms update (PR #832)**: clarified PDS delisting language in terms of service. 155 - 156 - **research**: documented emerging ATProto media service patterns from [community discourse](https://discourse.atprotocol.community/t/media-pds-service/297) — the ecosystem is converging on dedicated sidecar media services rather than PDS-as-media-host. our layered architecture (R2 + CDN + PDS records) aligns well. see `docs/research/2026-01-29-atproto-media-service-patterns.md`. 157 - 158 - --- 159 - 160 - #### PDS-based account creation (PRs #813-815, Jan 27) 161 - 162 - **create ATProto accounts directly from plyr.fm** - users without an existing ATProto identity can now create one during sign-up by selecting a PDS host. 110 + **mood search shipped** (PR #848): users can search by vibe/mood using natural language queries like "chill beats for studying". built on CLAP audio embeddings stored in turbopuffer, with a feature flag (`vibe-search`) and version-aware terms re-acceptance (privacy policy updated for ML processing). 163 111 164 112 **how it works**: 165 - - login page shows "create account" tab when feature is enabled 166 - - user selects a PDS (currently selfhosted.social) 167 - - OAuth flow uses `prompt=create` to trigger account creation on the PDS 168 - - after account creation, user is redirected back and logged in 113 + - text query → CLAP text embedding → turbopuffer cosine similarity → ranked tracks 114 + - results merged with keyword search in parallel, deduplicated by score 115 + - feature-flagged behind `vibe-search` user flag + terms version check 169 116 170 - **implementation details**: 171 - - `/auth/pds-options` endpoint returns available PDS hosts from config 172 - - `/auth/start` accepts `pds_url` parameter for account creation flow 173 - - handle resolution falls back to PDS directly (via `com.atproto.repo.describeRepo`) when Bluesky AppView hasn't indexed the new account yet 117 + **CLAP pipeline fixes (PRs #849-850)**: m4a support added (was failing on non-wav), R2 URL corrected, Modal API updated. embedding pipeline now normalizes similarity scores properly. 174 118 175 - **configuration** (`AccountCreationSettings`): 176 - - `enabled`: feature flag for account creation 177 - - `recommended_pds`: list of PDS options with name, url, and description 119 + **search quality hardening (PRs #851-858)**: 120 + - unified search runs keyword + semantic in parallel, merges by score instead of separating by type (#851, #858) 121 + - distance threshold + result cap to filter low-signal semantic matches (#852) 122 + - spread check filters results where all scores are similarly mediocre (#853) 123 + - switched CLAP model from `larger_clap_music` to `clap-htsat-unfused` (better quality, smaller) (#854) 124 + - handle empty turbopuffer namespace without error (#855) 125 + - concurrent backfill script, renamed "vibe search" → "mood search" throughout (#856) 126 + - cold start latency fix for search endpoint (#857) 178 127 179 128 --- 180 129 181 - #### lossless audio support (PRs #794-801, Jan 25) 130 + #### paginate portal tracks list (PR #878, Feb 8) 182 131 183 - **transcoding integration complete** - users can now upload AIFF and FLAC files. the system transcodes them to MP3 for browser compatibility while preserving originals for lossless playback. 184 - 185 - **how it works**: 186 - - upload AIFF/FLAC → original saved to R2, transcoded MP3 created 187 - - database stores both `file_id` (transcoded) and `original_file_id` (lossless) 188 - - frontend detects browser capabilities via `canPlayType()` 189 - - Safari/native apps get lossless, Chrome/Firefox get transcoded MP3 190 - - lossless badge shown on track cards when browser supports the format 191 - 192 - **key changes**: 193 - - `original_file_id` and `original_file_type` added to Track model and API 194 - - audio endpoint serves either version based on requested file_id 195 - - feature-flagged via `lossless-uploads` user flag 196 - 197 - **bug fixes during rollout**: 198 - - PR #796: audio endpoint now queries by `file_id` OR `original_file_id` 199 - - PR #797: store actual extension (`.aif`) not normalized format name (`.aiff`) 200 - 201 - **UI polish (PRs #799-801)**: 202 - - lossless badge positioned in top-right corner of track card (not artwork) 203 - - subtle glowing animation draws attention to premium quality tracks 204 - - whole card gets accent-colored border treatment when lossless 205 - - theme-aware styling, responsive sizing, respects `prefers-reduced-motion` 132 + portal tracks list now loads 25 tracks initially with a "load more" button instead of fetching all tracks at once. reduces initial page load for artists with large catalogs. 206 133 207 134 --- 208 135 209 - #### auth check optimization (PRs #781-782, Jan 23) 210 - 211 - **eliminated redundant /auth/me calls** - previously, every navigation triggered an auth check via the layout load function. for unauthenticated users, this meant a 401 on every page click (117 errors in 24 hours observed via Logfire). 212 - 213 - **fix**: auth singleton now tracks initialization state. `+layout.svelte` checks auth once on mount instead of every navigation. follow-up PR fixed library/liked pages that were broken by the layout simplification (they were using `await parent()` to get `isAuthenticated` which was no longer provided). 214 - 215 - --- 216 - 217 - #### remove SSR sensitive-images fetch (PR #785, Jan 24) 218 - 219 - **eliminated unnecessary SSR fetch** - the frontend SSR (`+layout.server.ts`) was fetching `/moderation/sensitive-images` on every page load to pre-populate the client-side moderation filter. during traffic spikes, this hammered the backend (1,179 rate limit hits over 7 days). 220 - 221 - **root cause**: the SSR fetch was premature optimization. cloudflare pages workers make direct fetch calls to fly.io - there's no CDN layer to cache responses. the cache-control headers we added in PR #784 only help browser caching, not SSR-to-origin requests. 222 - 223 - **fix**: removed the SSR fetch entirely. the client-side `ModerationManager` singleton already has caching and will fetch the data once on page load. the "flash of sensitive content" risk is theoretical - images load slower than a single API call completes, and there are only 2 flagged images. 224 - 225 - - deleted `+layout.server.ts` 226 - - simplified `+layout.ts` 227 - - updated pages to use `moderation.isSensitive()` singleton instead of SSR data 228 - 229 - --- 230 - 231 - #### listen receipts (PR #773, Jan 22) 136 + #### repo reorganization (PR #876, Feb 8) 232 137 233 - **share links now track who clicked and played** - when you share a track, you get a URL with a `?ref=` code that records visitors and listeners: 234 - - `POST /tracks/{id}/share` creates tracked share link with unique 8-character code (48 bits entropy) 235 - - frontend captures `?ref=` param on page load, fires click event to backend 236 - - play endpoint accepts optional `ref` param to record play attribution 237 - - `GET /tracks/me/shares` returns paginated stats: visitors, listeners, anonymous counts 238 - 239 - **portal share stats section**: 240 - - expandable cards per share link with copyable tracked URL 241 - - visitors (who clicked) and listeners (who played) shown as avatar circles 242 - - individual interaction counts per user 243 - - self-clicks/plays filtered out to avoid inflating stats 244 - 245 - **data model**: 246 - - `ShareLink` table: code, track_id, creator_did, created_at 247 - - `ShareLinkEvent` table: share_link_id, visitor_did (nullable for anonymous), event_type (click/play) 138 + moved `transcoder/`, `moderation/`, `redis/`, and `clap/` into `services/` and `infrastructure/` directories to match the documented project structure. updated all justfiles, Dockerfiles, GitHub Actions workflows, and deployment configs. 248 139 249 140 --- 250 141 251 - #### handle display fix (PR #774, Jan 22) 142 + #### smaller fixes (Feb 2-8) 252 143 253 - **DIDs were displaying instead of handles** in share link stats and other places (comments, track likers): 254 - - root cause: Artist records were only created during profile setup 255 - - users who authenticated but skipped setup had no Artist record 256 - - fix: create minimal Artist record (did, handle, avatar) during OAuth callback 257 - - profile setup now updates existing record instead of erroring 144 + - **mobile login handle hint** (PRs #843-845): prevent text wrap, hide hint text on mobile (show only service links), adaptively size based on viewport 145 + - **shared ShareButton** (PR #841): track detail page now uses the same ShareButton component as everywhere else instead of a one-off implementation 146 + - **PDS backfill feature flag** (PR #842): gate PDS backfill behind `pds-audio-uploads` feature flag (was previously accessible to all users) 147 + - **split genre/subgenre tags** (PR #870): genre predictions like "Electronic---Ambient" now produce two separate tags (`electronic`, `ambient`) instead of one combined tag 148 + - **remove duplicate toggle** (PR #874): duplicate auto-download toggle removed from playback settings section 149 + - **recommended tags endpoint** (PR #859, #861-862): `GET /tracks/{id}/recommended-tags` with turbopuffer vector fetch fix and namespace naming docs 258 150 259 151 --- 260 152 261 - #### responsive embed v2 (PRs #771-772, Jan 20-21) 262 - 263 - **complete rewrite of embed CSS** using container queries and proportional scaling: 264 - 265 - **layout modes**: 266 - - **wide** (width >= 400px): side art, proportional sizing 267 - - **very wide** (width >= 600px): larger art, more breathing room 268 - - **square/tall** (aspect <= 1.2, width >= 200px): art on top, 2-line titles 269 - - **very tall** (aspect <= 0.7, width >= 200px): blurred background overlay 270 - - **narrow** (width < 280px): compact blurred background 271 - - **micro** (width < 200px): hide time labels and logo 272 - 273 - **key technical changes**: 274 - - all sizes use `clamp()` with `cqi` units (container query units) 275 - - grid-based header layout instead of absolute positioning 276 - - gradient overlay (top-heavy to bottom-heavy) for text readability 277 - 278 - --- 279 - 280 - #### terms of service and privacy policy (PRs #567, #761-770, Jan 19-20) 281 - 282 - **legal foundation shipped** with ATProto-aware design: 283 - 284 - **terms cover**: 285 - - AT Protocol context (decentralized identity, user-controlled PDS) 286 - - content ownership (users retain ownership, plyr.fm gets license for streaming) 287 - - DMCA safe harbor with designated agent (DMCA-1069186) 288 - - federation disclaimer: audio files in blob storage we control, but ATProto records may persist on user's PDS 289 - 290 - **privacy policy**: 291 - - explicit third-party list with links (Cloudflare, Fly.io, Neon, Logfire, AudD, Anthropic, ATProtoFans) 292 - - data ownership clarity (DID, profile, tracks on user's PDS) 293 - - MIT license added to repo 294 - 295 - **acceptance flow** (TermsOverlay component): 296 - - shown on first login if `terms_accepted_at` is null 297 - - 4-bullet summary with links to full documents 298 - - "I Accept" or "Decline & Logout" options 299 - - `POST /account/accept-terms` records timestamp 300 - 301 - **polish PRs** (#761-770): corrected ATProto vs "our servers" terminology, standardized AT Protocol naming, added email fallbacks, capitalized sentence starts 302 - 303 - --- 304 - 305 - #### content gating research (Jan 18) 306 - 307 - researched ATProtoFans architecture and JSONLogic rule evaluation. documented findings in `docs/content-gating-roadmap.md`: 308 - - current ATProtoFans records and API (supporter, supporterProof, brokerProof, terms) 309 - - the gap: terms exist but aren't exposed via validateSupporter 310 - - how magazi uses datalogic-rs for flexible rule evaluation 311 - - open questions about upcoming metadata extensions 312 - 313 - no implementation changes - waiting to align with what ATProtoFans will support. 314 - 315 - #### logout modal UX (PRs #755-757, Jan 17-18) 316 - 317 - **tooltip scroll fix** (PR #755): 318 - - leftmost avatar in likers/commenters tooltip was clipped with no way to scroll to it 319 - - changed `justify-content: center` to `flex-start` so most recent (leftmost) is always visible 320 - 321 - **logout modal copy** (PRs #756-757): 322 - - simplified from two confusing questions to one clear question 323 - - before: "stay logged in?" + "you're logging out of @handle?" 324 - - after: "switch accounts?" 325 - - "logout completely" → "log out of all accounts" 326 - 327 - --- 328 - 329 - #### idempotent teal scrobbles (PR #754, Jan 16) 330 - 331 - **prevents duplicate scrobbles** when same play is submitted multiple times: 332 - - use `putRecord` with deterministic TID rkeys derived from `playedTime` instead of `createRecord` 333 - - network retries, multiple teal-compatible services, or background task retries won't create duplicates 334 - - adds `played_time` parameter to `build_teal_play_record` for deterministic record keys 335 - 336 - --- 337 - 338 - #### avatar refresh and tooltip polish (PRs #750-752, Jan 13) 339 - 340 - **avatar refresh from anywhere** (PR #751): 341 - - previously, stale avatar URLs were only fixed when visiting the artist detail page 342 - - now any broken avatar triggers a background refresh from Bluesky 343 - - shared `avatar-refresh.svelte.ts` provides global cache and request deduplication 344 - - works from: track items, likers tooltip, commenters tooltip, profile page 345 - 346 - **interactive tooltips** (PR #750): 347 - - hovering on like count shows avatar circles of users who liked 348 - - hovering on comment count shows avatar circles of commenters 349 - - lazy-loaded with 5-minute cache, invalidated when likes/comments change 350 - - elegant centered layout with horizontal scroll when needed 351 - 352 - **UX polish** (PR #752): 353 - - added prettier config with `useTabs: true` to match existing style 354 - - reduced avatar hover effect intensity (scale 1.2 → 1.08) 355 - - fixed avatar hover clipping at tooltip edge (added top padding) 356 - - track title now links to detail page (color change on hover) 357 - 358 - --- 359 - 360 - #### copyright flagging fix (PR #748, Jan 12) 361 - 362 - **switched from score-based to dominant match detection**: 363 - - AudD's enterprise API doesn't return confidence scores (always 0) 364 - - previous threshold-based detection was broken 365 - - new approach: flag if one song appears in >= 30% of matched segments 366 - - filters false positives where random segments match different songs 367 - 368 - --- 369 - 370 - #### Neon cold start fix (Jan 11) 371 - 372 - **why**: first requests after idle periods would fail with 500 errors due to Neon serverless scaling to zero after 5 minutes of inactivity. previous mitigations (larger pool, longer timeouts) helped but didn't eliminate the problem. 373 - 374 - **fix**: disabled scale-to-zero on `plyr-prd` via Neon console. this is the [recommended approach](https://neon.com/blog/6-best-practices-for-running-neon-in-production) for production workloads. 375 - 376 - **configuration**: 377 - - `plyr-prd`: scale-to-zero **disabled** (`suspend_timeout_seconds: -1`) 378 - - `plyr-stg`, `plyr-dev`: scale-to-zero enabled (cold starts acceptable) 379 - 380 - **docs**: updated [connection-pooling.md](docs/backend/database/connection-pooling.md) with production guidance and how to verify settings via Neon MCP. 381 - 382 - closes #733 383 - 384 - --- 385 - 386 - #### early January work (Jan 1-9) 153 + ### January 2026 387 154 388 155 See `.status_history/2026-01.md` for detailed history including: 389 - - multi-account experience (PRs #707, #710, #712-714, Jan 3-5) 390 - - integration test harness (PR #744, Jan 9) 391 - - track edit UX improvements (PRs #741-742, Jan 9) 392 - - auth stabilization (PRs #734-736, Jan 6-7) 393 - - timestamp deep links (PRs #739-740, Jan 8) 394 - - artist bio links (PRs #700-701, Jan 2) 395 - - copyright moderation improvements (PRs #703-704, Jan 2-3) 396 - - ATProto OAuth permission sets (PRs #697-698, Jan 1-2) 397 - - atprotofans supporters display (PRs #695-696, Jan 1) 398 - - UI polish (PRs #692-694, Dec 31 - Jan 1) 156 + - per-track PDS migration + UX polish (PRs #835-839, Jan 30-31) 157 + - PDS blob storage for audio (PRs #823-833, Jan 29) 158 + - PDS-based account creation (PRs #813-815, Jan 27) 159 + - lossless audio support (PRs #794-801, Jan 25) 160 + - auth check optimization (PRs #781-782, Jan 23) 161 + - remove SSR sensitive-images fetch (PR #785, Jan 24) 162 + - listen receipts (PR #773, Jan 22) 163 + - responsive embed v2 (PRs #771-772, Jan 20-21) 164 + - terms of service and privacy policy (PRs #567, #761-770, Jan 19-20) 165 + - content gating research, logout modal UX (Jan 16-18) 166 + - avatar refresh and tooltip polish (PRs #750-752, Jan 13) 167 + - copyright flagging fix (PR #748, Jan 12) 168 + - Neon cold start fix (Jan 11) 169 + - multi-account experience (PRs #707-714, Jan 3-5) 170 + - integration tests, track edit UX, auth stabilization (Jan 6-9) 171 + - artist bio links, copyright moderation, OAuth permission sets (Jan 1-3) 399 172 400 173 ### December 2025 401 174 ··· 440 213 441 214 ### current focus 442 215 443 - ML-powered track features rolling out: genre classification (Replicate effnet-discogs) auto-runs on upload, with optional auto-tagging checkbox on the upload form. mood search (CLAP embeddings + turbopuffer) feature-flagged behind `vibe-search`. ML audit script (`scripts/ml_audit.py`) tracks which tracks/artists have been processed for privacy/ToS compliance. 216 + ML-powered track features shipped: genre classification (Replicate effnet-discogs) auto-runs on upload with optional auto-tagging. mood search (CLAP embeddings + turbopuffer) feature-flagged behind `vibe-search` with unified keyword+semantic results. performance optimization pass on hot endpoints (GET /tracks/top p95 cut in half). portal pagination for large catalogs. repo reorganization to match documented project structure. 444 217 445 218 ### known issues 446 219 - iOS PWA audio may hang on first play after backgrounding ··· 494 267 - ✅ lossless audio (AIFF/FLAC) with automatic transcoding for browser compatibility 495 268 - ✅ PDS blob storage for audio (user data ownership) 496 269 - ✅ play count tracking, likes, queue management 497 - - ✅ unified search with Cmd/Ctrl+K 270 + - ✅ unified search with Cmd/Ctrl+K (keyword + mood/semantic in parallel) 498 271 - ✅ teal.fm scrobbling 499 272 - ✅ copyright moderation with ATProto labeler 500 273 - ✅ ML genre classification with suggested tags in edit modal + auto-tag at upload (Replicate effnet-discogs) 274 + - ✅ mood search via CLAP audio embeddings + turbopuffer (feature-flagged) 501 275 - ✅ docket background tasks (copyright scan, export, atproto sync, scrobble, genre classification) 502 276 - ✅ media export with concurrent downloads 503 277 - ✅ supporter-gated content via atprotofans ··· 542 316 - cloudflare (R2 + pages + domain): ~$1/month 543 317 - audd audio fingerprinting: $5-10/month (usage-based) 544 318 - replicate (genre classification): <$1/month (scales to zero, ~$0.00019/run) 319 + - modal (CLAP embeddings): usage-based (scales to zero) 320 + - turbopuffer (vector search): usage-based 545 321 - logfire: $0 (free tier) 546 322 547 323 ## admin tooling ··· 590 366 ├── frontend/ # SvelteKit app 591 367 │ ├── src/lib/ # components & state 592 368 │ └── src/routes/ # pages 593 - ├── moderation/ # Rust moderation service (ATProto labeler) 594 - ├── transcoder/ # Rust audio transcoding service 595 - ├── redis/ # self-hosted Redis config 369 + ├── services/ 370 + │ ├── transcoder/ # Rust audio transcoding (Fly.io) 371 + │ ├── moderation/ # Rust content moderation (Fly.io) 372 + │ └── clap/ # ML audio embeddings (Python, Modal) 373 + ├── infrastructure/ 374 + │ └── redis/ # self-hosted Redis (Fly.io) 596 375 ├── docs/ # documentation 597 376 └── justfile # task runner 598 377 ``` ··· 608 387 609 388 --- 610 389 611 - this is a living document. last updated 2026-02-08. 390 + this is a living document. last updated 2026-02-08 (status maintenance: archived January 2026 to `.status_history/2026-01.md`).