chore: status maintenance - supporter-gated content (#638)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

authored by claude[bot] claude[bot] Claude Opus 4.5 and committed by GitHub 44d2260b 99c02c16

Changed files
+254 -276
.status_history
+194
.status_history/2025-12.md
··· 412 412 - 6 original artists (people uploading their own distributed music) 413 413 414 414 **documentation**: see `docs/moderation/atproto-labeler.md` 415 + 416 + --- 417 + 418 + ## Mid-December 2025 Work (Dec 8-16) 419 + 420 + ### visual customization (PRs #595-596, Dec 16) 421 + 422 + **custom backgrounds** (PR #595): 423 + - users can set a custom background image URL in settings with optional tiling 424 + - new "playing artwork as background" toggle - uses current track's artwork as blurred page background 425 + - glass effect styling for track items (translucent backgrounds, subtle shadows) 426 + - new `ui_settings` JSONB column in preferences for extensible UI settings 427 + 428 + **bug fix** (PR #596): 429 + - removed 3D wheel scroll effect that was blocking like/share button clicks 430 + - root cause: `translateZ` transforms created z-index stacking that intercepted pointer events 431 + 432 + --- 433 + 434 + ### performance & UX polish (PRs #586-593, Dec 14-15) 435 + 436 + **performance improvements** (PRs #590-591): 437 + - removed moderation service call from `/tracks/` listing endpoint 438 + - removed copyright check from tag listing endpoint 439 + - faster page loads for track feeds 440 + 441 + **moderation agent** (PRs #586, #588): 442 + - added moderation agent script with audit trail support 443 + - improved moderation prompt and UI layout 444 + 445 + **bug fixes** (PRs #589, #592, #593): 446 + - fixed liked state display on playlist detail page 447 + - preserved album track order during ATProto sync 448 + - made header sticky on scroll for better mobile navigation 449 + 450 + **iOS Safari fixes** (PRs #573-576): 451 + - fixed AddToMenu visibility issue on iOS Safari 452 + - menu now correctly opens upward when near viewport bottom 453 + 454 + --- 455 + 456 + ### mobile UI polish & background task expansion (PRs #558-572, Dec 10-12) 457 + 458 + **background task expansion** (PRs #558, #561): 459 + - moved like/unlike and comment PDS writes to docket background tasks 460 + - API responses now immediate; PDS sync happens asynchronously 461 + - added targeted album list sync background task for ATProto record updates 462 + 463 + **performance caching** (PR #566): 464 + - added Redis cache for copyright label lookups (5-minute TTL) 465 + - fixed 2-3s latency spikes on `/tracks/` endpoint 466 + - batch operations via `mget`/pipeline for efficiency 467 + 468 + **mobile UX improvements** (PRs #569, #572): 469 + - mobile action menus now open from top with all actions visible 470 + - UI polish for album and artist pages on small screens 471 + 472 + **misc** (PRs #559, #562, #563, #570): 473 + - reduced docket Redis polling from 250ms to 5s (lower resource usage) 474 + - added atprotofans support link mode for ko-fi integration 475 + - added alpha badge to header branding 476 + - fixed web manifest ID for PWA stability 477 + 478 + --- 479 + 480 + ### confidential OAuth client (PRs #578, #580-582, Dec 12-13) 481 + 482 + **confidential client support** (PR #578): 483 + - implemented ATProto OAuth confidential client using `private_key_jwt` authentication 484 + - when `OAUTH_JWK` is configured, plyr.fm authenticates with a cryptographic key 485 + - confidential clients earn 180-day refresh tokens (vs 2-week for public clients) 486 + - added `/.well-known/jwks.json` endpoint for public key discovery 487 + - updated `/oauth-client-metadata.json` with confidential client fields 488 + 489 + **bug fixes** (PRs #580-582): 490 + - fixed client assertion JWT to use Authorization Server's issuer as `aud` claim (not token endpoint URL) 491 + - fixed JWKS endpoint to preserve `kid` field from original JWK 492 + - fixed `OAuthClient` to pass `client_secret_kid` for JWT header 493 + 494 + **atproto fork updates** (zzstoatzz/atproto#6, #7): 495 + - added `issuer` parameter to `_make_token_request()` for correct `aud` claim 496 + - added `client_secret_kid` parameter to include `kid` in client assertion JWT header 497 + 498 + **outcome**: users now get 180-day refresh tokens, and "remember this account" on the PDS authorization page works (auto-approves subsequent logins). see #583 for future work on account switching via OAuth `prompt` parameter. 499 + 500 + --- 501 + 502 + ### pagination & album management (PRs #550-554, Dec 9-10) 503 + 504 + **tracks list pagination** (PR #554): 505 + - cursor-based pagination on `/tracks/` endpoint (default 50 per page) 506 + - infinite scroll on homepage using native IntersectionObserver 507 + - zero new dependencies - uses browser APIs only 508 + - pagination state persisted to localStorage for fast subsequent loads 509 + 510 + **album management improvements** (PRs #550-552, #557): 511 + - album delete and track reorder fixes 512 + - album page edit mode matching playlist UX (inline title editing, cover upload) 513 + - optimistic UI updates for album title changes (instant feedback) 514 + - ATProto record sync when album title changes (updates all track records + list record) 515 + - fixed album slug sync on rename (prevented duplicate albums when adding tracks) 516 + 517 + **playlist show on profile** (PR #553): 518 + - restored "show on profile" toggle that was lost during inline editing refactor 519 + - users can now control whether playlists appear on their public profile 520 + 521 + --- 522 + 523 + ### public cost dashboard (PRs #548-549, Dec 9) 524 + 525 + - `/costs` page showing live platform infrastructure costs 526 + - daily export to R2 via GitHub Action, proxied through `/stats/costs` endpoint 527 + - dedicated `plyr-stats` R2 bucket with public access (shared across environments) 528 + - includes fly.io, neon, cloudflare, and audd API costs 529 + - ko-fi integration for community support 530 + 531 + ### docket background tasks & concurrent exports (PRs #534-546, Dec 9) 532 + 533 + **docket integration** (PRs #534, #536, #539): 534 + - migrated background tasks from inline asyncio to docket (Redis-backed task queue) 535 + - copyright scanning, media export, ATProto sync, and teal scrobbling now run via docket 536 + - graceful fallback to asyncio for local development without Redis 537 + - parallel test execution with xdist template databases (#540) 538 + 539 + **concurrent export downloads** (PR #545): 540 + - exports now download tracks in parallel (up to 4 concurrent) instead of sequentially 541 + - significantly faster for users with many tracks or large files 542 + - zip creation remains sequential (zipfile constraint) 543 + 544 + **ATProto refactor** (PR #534): 545 + - reorganized ATProto record code into `_internal/atproto/records/` by lexicon namespace 546 + - extracted `client.py` for low-level PDS operations 547 + - cleaner separation between plyr.fm and teal.fm lexicons 548 + 549 + **documentation & observability**: 550 + - AudD API cost tracking dashboard (#546) 551 + - promoted runbooks from sandbox to `docs/runbooks/` 552 + - updated CLAUDE.md files across the codebase 553 + 554 + --- 555 + 556 + ### artist support links & inline playlist editing (PRs #520-532, Dec 8) 557 + 558 + **artist support link** (PR #532): 559 + - artists can set a support URL (Ko-fi, Patreon, etc.) in their portal profile 560 + - support link displays as a button on artist profile pages next to the share button 561 + - URLs validated to require https:// prefix 562 + 563 + **inline playlist editing** (PR #531): 564 + - edit playlist name and description directly on playlist detail page 565 + - click-to-upload cover art replacement without modal 566 + - cleaner UX - no more edit modal popup 567 + 568 + **platform stats enhancements** (PRs #522, #528): 569 + - total duration displayed in platform stats (e.g., "42h 15m of music") 570 + - duration shown per artist in analytics section 571 + - combined stats and search into single centered container for cleaner layout 572 + 573 + **navigation & data loading fixes** (PR #527): 574 + - fixed stale data when navigating between detail pages of the same type 575 + - e.g., clicking from one artist to another now properly reloads data 576 + 577 + **copyright moderation improvements** (PR #480): 578 + - enhanced moderation workflow for copyright claims 579 + - improved labeler integration 580 + 581 + **status maintenance workflow** (PR #529): 582 + - automated status maintenance using claude-code-action 583 + - reviews merged PRs and updates STATUS.md narratively 584 + 585 + --- 586 + 587 + ### playlist fast-follow fixes (PRs #507-519, Dec 7-8) 588 + 589 + **public playlist viewing** (PR #519): 590 + - playlists now publicly viewable without authentication 591 + - ATProto records are public by design - auth was unnecessary for read access 592 + - shared playlist URLs no longer redirect unauthenticated users to homepage 593 + 594 + **inline playlist creation** (PR #510): 595 + - clicking "create new playlist" from AddToMenu previously navigated to `/library?create=playlist` 596 + - this caused SvelteKit to reinitialize the layout, destroying the audio element and stopping playback 597 + - fix: added inline create form that creates playlist and adds track in one action without navigation 598 + 599 + **UI polish** (PRs #507-509, #515): 600 + - include `image_url` in playlist SSR data for og:image link previews 601 + - invalidate layout data after token exchange - fixes stale auth state after login 602 + - fixed stopPropagation blocking "create new playlist" link clicks 603 + - detail page button layouts: all buttons visible on mobile, centered AddToMenu on track detail 604 + - AddToMenu smart positioning: menu opens upward when near viewport bottom 605 + 606 + **documentation** (PR #514): 607 + - added lexicons overview documentation at `docs/lexicons/overview.md` 608 + - covers `fm.plyr.track`, `fm.plyr.like`, `fm.plyr.comment`, `fm.plyr.list`, `fm.plyr.actor.profile`
+60 -276
STATUS.md
··· 47 47 48 48 ### December 2025 49 49 50 - #### rate limit moderation endpoint (PR #629, Dec 21) 50 + #### supporter-gated content (PR #637, Dec 22-23) 51 51 52 - **incident response**: detected suspicious activity - 72 requests in 17 seconds from a single IP targeting `/moderation/sensitive-images`. investigation via Logfire showed: 53 - - single IP generating all traffic with no User-Agent header 54 - - requests spaced ~230ms apart (too consistent for human browsing) 55 - - no corresponding user activity (page loads, audio streams) 52 + **atprotofans paywall integration** - artists can now mark tracks as "supporters only": 53 + - tracks with `support_gate` require atprotofans validation before playback 54 + - non-supporters see lock icon and "become a supporter" CTA linking to atprotofans 55 + - artists can always play their own gated tracks 56 56 57 - **fix**: added `10/minute` rate limit to the endpoint using existing slowapi infrastructure. verified rate limiting works correctly post-deployment. 57 + **backend architecture**: 58 + - audio endpoint validates supporter status via atprotofans API before serving gated content 59 + - HEAD requests return 200/401/402 for pre-flight auth checks (avoids CORS issues) 60 + - `R2Storage.move_audio()` moves files between public/private buckets when toggling gate 61 + - background task handles bucket migration asynchronously 62 + - ATProto record syncs when toggling gate (updates `supportGate` field and `audioUrl`) 63 + 64 + **frontend**: 65 + - `playback.svelte.ts` guards queue operations with gated checks BEFORE modifying state 66 + - clicking locked track shows toast with CTA - does NOT interrupt current playback 67 + - portal shows support gate toggle in track edit UI 58 68 59 69 --- 60 70 61 - #### end-of-year sprint (Dec 20-31) 71 + #### supporter badges (PR #627, Dec 21-22) 72 + 73 + **phase 1 of atprotofans integration**: 74 + - supporter badge displays on artist pages when logged-in viewer supports the artist 75 + - calls atprotofans `validateSupporter` API directly from frontend (public endpoint) 76 + - badge only shows when viewer is authenticated and not viewing their own profile 62 77 63 - **focus**: two foundational systems need solid experimental implementations by 2026. 78 + --- 64 79 65 - **track 1: moderation architecture overhaul** 66 - - consolidate sensitive images into moderation service 67 - - add event-sourced audit trail 68 - - implement configurable rules (replace hard-coded thresholds) 69 - - informed by [Roost Osprey](https://github.com/roostorg/osprey) patterns and [Bluesky Ozone](https://github.com/bluesky-social/ozone) workflows 80 + #### rate limit moderation endpoint (PR #629, Dec 21) 70 81 71 - **track 2: atprotofans paywall integration** 72 - - phase 1: read-only supporter validation (show badges) 73 - - phase 2: platform registration (artists create support tiers) 74 - - phase 3: content gating (track-level access control) 82 + **incident response**: detected suspicious activity - 72 requests in 17 seconds from a single IP targeting `/moderation/sensitive-images`. added `10/minute` rate limit using existing slowapi infrastructure. 83 + 84 + --- 85 + 86 + #### end-of-year sprint planning (PR #626, Dec 20) 87 + 88 + **focus**: two foundational systems need solid experimental implementations by 2026. 89 + 90 + | track | focus | status | 91 + |-------|-------|--------| 92 + | moderation | consolidate architecture, add rules engine | in progress | 93 + | atprotofans | supporter validation, content gating | shipped (phase 1-3) | 75 94 76 95 **research docs**: 77 96 - [moderation architecture overhaul](docs/research/2025-12-20-moderation-architecture-overhaul.md) 78 97 - [atprotofans paywall integration](docs/research/2025-12-20-atprotofans-paywall-integration.md) 79 98 80 - **tracking**: issue #625 81 - 82 99 --- 83 100 84 101 #### beartype + moderation cleanup (PRs #617-619, Dec 19) ··· 87 104 - enabled beartype runtime type validation across the backend 88 105 - catches type errors at runtime instead of silently passing bad data 89 106 - test infrastructure improvements: session-scoped TestClient fixture (5x faster tests) 90 - - disabled automatic perpetual task scheduling in tests 91 107 92 108 **moderation cleanup** (PRs #617-618): 93 109 - consolidated moderation code, addressing issues #541-543 94 110 - `sync_copyright_resolutions` now runs automatically via docket Perpetual task 95 - - removed `init_db()` from lifespan (handled by alembic migrations) 111 + - removed dead `init_db()` from lifespan (handled by alembic migrations) 96 112 97 113 --- 98 114 ··· 111 127 - mobile modals now use full screen positioning 112 128 - fixed `/tag/` routes in hasPageMetadata check 113 129 114 - **misc** (PRs #598-601): 115 - - upload button added to desktop header nav 116 - - background settings UX improvements 117 - - switched support link to atprotofans 118 - - AudD costs now derived from track duration for accurate billing 119 - 120 130 --- 121 131 122 132 #### offline mode foundation (PRs #610-611, Dec 17) 123 133 124 134 **experimental offline playback**: 125 - - new storage layer using Cache API for audio bytes + IndexedDB for metadata 135 + - storage layer using Cache API for audio bytes + IndexedDB for metadata 126 136 - `GET /audio/{file_id}/url` backend endpoint returns direct R2 URLs for client-side caching 127 137 - "auto-download liked" toggle in experimental settings section 128 - - when enabled, bulk-downloads all liked tracks and auto-downloads future likes 129 138 - Player checks for cached audio before streaming from R2 130 - - works offline once tracks are downloaded 131 - 132 - **robustness improvements**: 133 - - IndexedDB connections properly closed after each operation 134 - - concurrent downloads deduplicated via in-flight promise tracking 135 - - stale metadata cleanup when cache entries are missing 136 139 137 140 --- 138 141 139 - #### visual customization (PRs #595-596, Dec 16) 140 - 141 - **custom backgrounds** (PR #595): 142 - - users can set a custom background image URL in settings with optional tiling 143 - - new "playing artwork as background" toggle - uses current track's artwork as blurred page background 144 - - glass effect styling for track items (translucent backgrounds, subtle shadows) 145 - - new `ui_settings` JSONB column in preferences for extensible UI settings 146 - 147 - **bug fix** (PR #596): 148 - - removed 3D wheel scroll effect that was blocking like/share button clicks 149 - - root cause: `translateZ` transforms created z-index stacking that intercepted pointer events 150 - 151 - --- 152 - 153 - #### performance & UX polish (PRs #586-593, Dec 14-15) 154 - 155 - **performance improvements** (PRs #590-591): 156 - - removed moderation service call from `/tracks/` listing endpoint 157 - - removed copyright check from tag listing endpoint 158 - - faster page loads for track feeds 159 - 160 - **moderation agent** (PRs #586, #588): 161 - - added moderation agent script with audit trail support 162 - - improved moderation prompt and UI layout 163 - 164 - **bug fixes** (PRs #589, #592, #593): 165 - - fixed liked state display on playlist detail page 166 - - preserved album track order during ATProto sync 167 - - made header sticky on scroll for better mobile navigation 168 - 169 - **iOS Safari fixes** (PRs #573-576): 170 - - fixed AddToMenu visibility issue on iOS Safari 171 - - menu now correctly opens upward when near viewport bottom 172 - 173 - --- 174 - 175 - #### mobile UI polish & background task expansion (PRs #558-572, Dec 10-12) 176 - 177 - **background task expansion** (PRs #558, #561): 178 - - moved like/unlike and comment PDS writes to docket background tasks 179 - - API responses now immediate; PDS sync happens asynchronously 180 - - added targeted album list sync background task for ATProto record updates 181 - 182 - **performance caching** (PR #566): 183 - - added Redis cache for copyright label lookups (5-minute TTL) 184 - - fixed 2-3s latency spikes on `/tracks/` endpoint 185 - - batch operations via `mget`/pipeline for efficiency 186 - 187 - **mobile UX improvements** (PRs #569, #572): 188 - - mobile action menus now open from top with all actions visible 189 - - UI polish for album and artist pages on small screens 190 - 191 - **misc** (PRs #559, #562, #563, #570): 192 - - reduced docket Redis polling from 250ms to 5s (lower resource usage) 193 - - added atprotofans support link mode for ko-fi integration 194 - - added alpha badge to header branding 195 - - fixed web manifest ID for PWA stability 196 - 197 - --- 198 - 199 - #### confidential OAuth client (PRs #578, #580-582, Dec 12-13) 200 - 201 - **confidential client support** (PR #578): 202 - - implemented ATProto OAuth confidential client using `private_key_jwt` authentication 203 - - when `OAUTH_JWK` is configured, plyr.fm authenticates with a cryptographic key 204 - - confidential clients earn 180-day refresh tokens (vs 2-week for public clients) 205 - - added `/.well-known/jwks.json` endpoint for public key discovery 206 - - updated `/oauth-client-metadata.json` with confidential client fields 207 - 208 - **bug fixes** (PRs #580-582): 209 - - fixed client assertion JWT to use Authorization Server's issuer as `aud` claim (not token endpoint URL) 210 - - fixed JWKS endpoint to preserve `kid` field from original JWK 211 - - fixed `OAuthClient` to pass `client_secret_kid` for JWT header 212 - 213 - **atproto fork updates** (zzstoatzz/atproto#6, #7): 214 - - added `issuer` parameter to `_make_token_request()` for correct `aud` claim 215 - - added `client_secret_kid` parameter to include `kid` in client assertion JWT header 216 - 217 - **outcome**: users now get 180-day refresh tokens, and "remember this account" on the PDS authorization page works (auto-approves subsequent logins). see #583 for future work on account switching via OAuth `prompt` parameter. 218 - 219 - --- 220 - 221 - #### pagination & album management (PRs #550-554, Dec 9-10) 222 - 223 - **tracks list pagination** (PR #554): 224 - - cursor-based pagination on `/tracks/` endpoint (default 50 per page) 225 - - infinite scroll on homepage using native IntersectionObserver 226 - - zero new dependencies - uses browser APIs only 227 - - pagination state persisted to localStorage for fast subsequent loads 228 - 229 - **album management improvements** (PRs #550-552, #557): 230 - - album delete and track reorder fixes 231 - - album page edit mode matching playlist UX (inline title editing, cover upload) 232 - - optimistic UI updates for album title changes (instant feedback) 233 - - ATProto record sync when album title changes (updates all track records + list record) 234 - - fixed album slug sync on rename (prevented duplicate albums when adding tracks) 235 - 236 - **playlist show on profile** (PR #553): 237 - - restored "show on profile" toggle that was lost during inline editing refactor 238 - - users can now control whether playlists appear on their public profile 239 - 240 - --- 241 - 242 - #### public cost dashboard (PRs #548-549, Dec 9) 243 - 244 - - `/costs` page showing live platform infrastructure costs 245 - - daily export to R2 via GitHub Action, proxied through `/stats/costs` endpoint 246 - - dedicated `plyr-stats` R2 bucket with public access (shared across environments) 247 - - includes fly.io, neon, cloudflare, and audd API costs 248 - - ko-fi integration for community support 249 - 250 - #### docket background tasks & concurrent exports (PRs #534-546, Dec 9) 251 - 252 - **docket integration** (PRs #534, #536, #539): 253 - - migrated background tasks from inline asyncio to docket (Redis-backed task queue) 254 - - copyright scanning, media export, ATProto sync, and teal scrobbling now run via docket 255 - - graceful fallback to asyncio for local development without Redis 256 - - parallel test execution with xdist template databases (#540) 257 - 258 - **concurrent export downloads** (PR #545): 259 - - exports now download tracks in parallel (up to 4 concurrent) instead of sequentially 260 - - significantly faster for users with many tracks or large files 261 - - zip creation remains sequential (zipfile constraint) 262 - 263 - **ATProto refactor** (PR #534): 264 - - reorganized ATProto record code into `_internal/atproto/records/` by lexicon namespace 265 - - extracted `client.py` for low-level PDS operations 266 - - cleaner separation between plyr.fm and teal.fm lexicons 267 - 268 - **documentation & observability**: 269 - - AudD API cost tracking dashboard (#546) 270 - - promoted runbooks from sandbox to `docs/runbooks/` 271 - - updated CLAUDE.md files across the codebase 272 - 273 - --- 274 - 275 - #### artist support links & inline playlist editing (PRs #520-532, Dec 8) 276 - 277 - **artist support link** (PR #532): 278 - - artists can set a support URL (Ko-fi, Patreon, etc.) in their portal profile 279 - - support link displays as a button on artist profile pages next to the share button 280 - - URLs validated to require https:// prefix 281 - 282 - **inline playlist editing** (PR #531): 283 - - edit playlist name and description directly on playlist detail page 284 - - click-to-upload cover art replacement without modal 285 - - cleaner UX - no more edit modal popup 286 - 287 - **platform stats enhancements** (PRs #522, #528): 288 - - total duration displayed in platform stats (e.g., "42h 15m of music") 289 - - duration shown per artist in analytics section 290 - - combined stats and search into single centered container for cleaner layout 291 - 292 - **navigation & data loading fixes** (PR #527): 293 - - fixed stale data when navigating between detail pages of the same type 294 - - e.g., clicking from one artist to another now properly reloads data 295 - 296 - **copyright moderation improvements** (PR #480): 297 - - enhanced moderation workflow for copyright claims 298 - - improved labeler integration 299 - 300 - **status maintenance workflow** (PR #529): 301 - - automated status maintenance using claude-code-action 302 - - reviews merged PRs and updates STATUS.md narratively 303 - 304 - --- 305 - 306 - #### playlist fast-follow fixes (PRs #507-519, Dec 7-8) 307 - 308 - **public playlist viewing** (PR #519): 309 - - playlists now publicly viewable without authentication 310 - - ATProto records are public by design - auth was unnecessary for read access 311 - - shared playlist URLs no longer redirect unauthenticated users to homepage 312 - 313 - **inline playlist creation** (PR #510): 314 - - clicking "create new playlist" from AddToMenu previously navigated to `/library?create=playlist` 315 - - this caused SvelteKit to reinitialize the layout, destroying the audio element and stopping playback 316 - - fix: added inline create form that creates playlist and adds track in one action without navigation 317 - 318 - **UI polish** (PRs #507-509, #515): 319 - - include `image_url` in playlist SSR data for og:image link previews 320 - - invalidate layout data after token exchange - fixes stale auth state after login 321 - - fixed stopPropagation blocking "create new playlist" link clicks 322 - - detail page button layouts: all buttons visible on mobile, centered AddToMenu on track detail 323 - - AddToMenu smart positioning: menu opens upward when near viewport bottom 324 - 325 - **documentation** (PR #514): 326 - - added lexicons overview documentation at `docs/lexicons/overview.md` 327 - - covers `fm.plyr.track`, `fm.plyr.like`, `fm.plyr.comment`, `fm.plyr.list`, `fm.plyr.actor.profile` 328 - 329 - --- 330 - 331 - #### playlists, ATProto sync, and library hub (PR #499, Dec 6-7) 332 - 333 - **playlists** (full CRUD): 334 - - create, rename, delete playlists with cover art upload 335 - - add/remove/reorder tracks with drag-and-drop 336 - - playlist detail page with edit modal 337 - - "add to playlist" menu on tracks with inline create 338 - - playlist sharing with OpenGraph link previews 339 - 340 - **ATProto integration**: 341 - - `fm.plyr.list` lexicon for syncing playlists/albums to user PDSes 342 - - `fm.plyr.actor.profile` lexicon for artist profiles 343 - - automatic sync of albums, liked tracks, profile on login 344 - 345 - **library hub** (`/library`): 346 - - unified page with tabs: liked, playlists, albums 347 - - nav changed from "liked" → "library" 348 - 349 - **related**: scope upgrade OAuth flow (PR #503), settings consolidation (PR #496) 350 - 351 - --- 352 - 353 - #### sensitive image moderation (PRs #471-488, Dec 5-6) 354 - 355 - - `sensitive_images` table flags problematic images 356 - - `show_sensitive_artwork` user preference 357 - - flagged images blurred everywhere: track lists, player, artist pages, search, embeds 358 - - Media Session API respects sensitive preference 359 - - SSR-safe filtering for og:image link previews 142 + ### Earlier December 2025 360 143 361 - --- 362 - 363 - #### teal.fm scrobbling (PR #467, Dec 4) 364 - 365 - - native scrobbling to user's PDS using teal's ATProto lexicons 366 - - scrobble at 30% or 30 seconds (same threshold as play counts) 367 - - toggle in settings, link to pdsls.dev to view records 368 - 369 - --- 144 + See `.status_history/2025-12.md` for detailed history including: 145 + - visual customization with custom backgrounds (PRs #595-596, Dec 16) 146 + - performance & moderation polish (PRs #586-593, Dec 14-15) 147 + - mobile UI polish & background task expansion (PRs #558-572, Dec 10-12) 148 + - confidential OAuth client for 180-day sessions (PRs #578-582, Dec 12-13) 149 + - pagination & album management (PRs #550-554, Dec 9-10) 150 + - public cost dashboard (PRs #548-549, Dec 9) 151 + - docket background tasks & concurrent exports (PRs #534-546, Dec 9) 152 + - artist support links & inline playlist editing (PRs #520-532, Dec 8) 153 + - playlist fast-follow fixes (PRs #507-519, Dec 7-8) 154 + - playlists, ATProto sync, and library hub (PR #499, Dec 6-7) 155 + - sensitive image moderation (PRs #471-488, Dec 5-6) 156 + - teal.fm scrobbling (PR #467, Dec 4) 157 + - unified search with Cmd+K (PR #447, Dec 3) 158 + - light/dark theme system (PR #441, Dec 2-3) 159 + - tag filtering and bufo easter egg (PRs #431-438, Dec 2) 370 160 371 - ### Earlier December / November 2025 161 + ### November 2025 372 162 373 - See `.status_history/2025-12.md` and `.status_history/2025-11.md` for detailed history including: 374 - - unified search with Cmd+K (PR #447) 375 - - light/dark theme system (PR #441) 376 - - tag filtering and bufo easter egg (PRs #431-438) 163 + See `.status_history/2025-11.md` for detailed history including: 377 164 - developer tokens (PR #367) 378 165 - copyright moderation system (PRs #382-395) 379 166 - export & upload reliability (PRs #337-344) ··· 388 175 | track | focus | status | 389 176 |-------|-------|--------| 390 177 | moderation | consolidate architecture, add rules engine | planning | 391 - | atprotofans | supporter validation, content gating | planning | 178 + | atprotofans | supporter validation, content gating | shipped | 392 179 393 180 ### known issues 394 181 - playback auto-start on refresh (#225) ··· 443 230 - ✅ copyright moderation with ATProto labeler 444 231 - ✅ docket background tasks (copyright scan, export, atproto sync, scrobble) 445 232 - ✅ media export with concurrent downloads 233 + - ✅ supporter-gated content via atprotofans 446 234 447 235 **albums** 448 236 - ✅ album CRUD with cover art ··· 479 267 see live dashboard: [plyr.fm/costs](https://plyr.fm/costs) 480 268 481 269 - fly.io (plyr apps only): ~$12/month 482 - - relay-api (prod): $5.80 483 - - relay-api-staging: $5.60 484 - - plyr-moderation: $0.24 485 - - plyr-transcoder: $0.02 486 270 - neon postgres: $5/month 487 271 - cloudflare (R2 + pages + domain): ~$1.16/month 488 272 - audd audio fingerprinting: $0-10/month (6000 free/month) ··· 551 335 552 336 --- 553 337 554 - this is a living document. last updated 2025-12-21. 338 + this is a living document. last updated 2025-12-23.
update.wav

This is a binary file and will not be displayed.