chore: status maintenance - multi-account and copyright compliance (#728)

🤖 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 4c74e12e f0c01d18

Changed files
+176 -164
.status_history
+163
.status_history/2025-12.md
··· 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`
··· 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` 609 + 610 + --- 611 + 612 + ## Late December 2025 Work (Dec 17-31) 613 + 614 + ### offline mode foundation (PRs #610-611, Dec 17) 615 + 616 + **experimental offline playback**: 617 + - storage layer using Cache API for audio bytes + IndexedDB for metadata 618 + - `GET /audio/{file_id}/url` backend endpoint returns direct R2 URLs for client-side caching 619 + - "auto-download liked" toggle in experimental settings section 620 + - Player checks for cached audio before streaming from R2 621 + 622 + --- 623 + 624 + ### UX polish (PRs #604-607, #613, #615, Dec 16-18) 625 + 626 + **login improvements** (PRs #604, #613): 627 + - login page now uses "internet handle" terminology for clarity 628 + - input normalization: strips `@` and `at://` prefixes automatically 629 + 630 + **artist page fixes** (PR #615): 631 + - track pagination on artist pages now works correctly 632 + - fixed mobile album card overflow 633 + 634 + **mobile + metadata** (PRs #605-607): 635 + - Open Graph tags added to tag detail pages for link previews 636 + - mobile modals now use full screen positioning 637 + - fixed `/tag/` routes in hasPageMetadata check 638 + 639 + --- 640 + 641 + ### beartype + moderation cleanup (PRs #617-619, Dec 19) 642 + 643 + **runtime type checking** (PR #619): 644 + - enabled beartype runtime type validation across the backend 645 + - catches type errors at runtime instead of silently passing bad data 646 + - test infrastructure improvements: session-scoped TestClient fixture (5x faster tests) 647 + 648 + **moderation cleanup** (PRs #617-618): 649 + - consolidated moderation code, addressing issues #541-543 650 + - `sync_copyright_resolutions` now runs automatically via docket Perpetual task 651 + - removed dead `init_db()` from lifespan (handled by alembic migrations) 652 + 653 + --- 654 + 655 + ### end-of-year sprint (PR #626, Dec 20) 656 + 657 + **focus**: two foundational systems with experimental implementations. 658 + 659 + | track | focus | status | 660 + |-------|-------|--------| 661 + | moderation | consolidate architecture, batch review, Claude vision | shipped | 662 + | atprotofans | supporter validation, content gating | shipped | 663 + 664 + **research docs**: 665 + - [moderation architecture overhaul](docs/research/2025-12-20-moderation-architecture-overhaul.md) 666 + - [atprotofans paywall integration](docs/research/2025-12-20-atprotofans-paywall-integration.md) 667 + 668 + --- 669 + 670 + ### rate limit moderation endpoint (PR #629, Dec 21) 671 + 672 + **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. this was the first real probe of our moderation endpoints, validating the decision to add rate limiting before it became a problem. 673 + 674 + --- 675 + 676 + ### supporter badges (PR #627, Dec 21-22) 677 + 678 + **phase 1 of atprotofans integration**: 679 + - supporter badge displays on artist pages when logged-in viewer supports the artist 680 + - calls atprotofans `validateSupporter` API directly from frontend (public endpoint) 681 + - badge only shows when viewer is authenticated and not viewing their own profile 682 + 683 + --- 684 + 685 + ### supporter-gated content (PR #637, Dec 22-23) 686 + 687 + **atprotofans paywall integration** - artists can now mark tracks as "supporters only": 688 + - tracks with `support_gate` require atprotofans validation before playback 689 + - non-supporters see lock icon and "become a supporter" CTA linking to atprotofans 690 + - artists can always play their own gated tracks 691 + 692 + **backend architecture**: 693 + - audio endpoint validates supporter status via atprotofans API before serving gated content 694 + - HEAD requests return 200/401/402 for pre-flight auth checks (avoids CORS issues with cross-origin redirects) 695 + - gated files stored in private R2 bucket, served via presigned URLs (SigV4 signatures) 696 + - `R2Storage.move_audio()` moves files between public/private buckets when toggling gate 697 + - background task handles bucket migration asynchronously 698 + - ATProto record syncs when toggling gate (updates `supportGate` field and `audioUrl` to point at our endpoint instead of R2) 699 + 700 + **frontend**: 701 + - `playback.svelte.ts` guards queue operations with gated checks BEFORE modifying state 702 + - clicking locked track shows toast with CTA - does NOT interrupt current playback 703 + - portal shows support gate toggle in track edit UI 704 + 705 + **key decision**: gated status is resolved server-side in track listings, not client-side. this means the lock icon appears instantly without additional API calls, and prevents information leakage about which tracks are gated vs which the user simply can't access. 706 + 707 + --- 708 + 709 + ### CSS design tokens (PRs #662-664, Dec 29-30) 710 + 711 + **design system foundations**: 712 + - border-radius tokens (`--radius-sm`, `--radius-md`, etc.) 713 + - typography scale tokens 714 + - consolidated form styles 715 + - documented in `docs/frontend/design-tokens.md` 716 + 717 + --- 718 + 719 + ### self-hosted redis (PRs #674-675, Dec 30) 720 + 721 + **replaced Upstash with self-hosted Redis on Fly.io** - ~$75/month → ~$4/month: 722 + - Upstash pay-as-you-go was charging per command (37M commands = $75) - discovered when reviewing December costs 723 + - docket's heartbeat mechanism is chatty by design, making pay-per-command pricing unsuitable 724 + - self-hosted Redis on 256MB Fly VMs costs fixed ~$2/month per environment 725 + - deployed `plyr-redis` (prod) and `plyr-redis-stg` (staging) 726 + - added CI workflow for redis deployments on merge 727 + 728 + **no state migration needed** - docket stores ephemeral task queue data, job progress lives in postgres. 729 + 730 + **incident (Dec 30)**: while optimizing redis overhead, a `heartbeat_interval=30s` change broke docket task execution. likes created Dec 29-30 were missing ATProto records. reverted in PR #669, documented in `docs/backend/background-tasks.md`. filed upstream: https://github.com/chrisguidry/docket/issues/267 731 + 732 + --- 733 + 734 + ### batch review system (PR #672, Dec 30) 735 + 736 + **moderation batch review UI** - mobile-friendly interface for reviewing flagged content: 737 + - filter by flag status, paginated results 738 + - auto-resolve flags for deleted tracks (PR #681) 739 + - full URL in DM notifications (PR #678) 740 + - required auth flow fix (PR #679) - review page was accessible without login 741 + 742 + --- 743 + 744 + ### top tracks homepage (PR #684, Dec 31) 745 + 746 + **homepage now shows top tracks** - quick access to popular content for new visitors. 747 + 748 + --- 749 + 750 + ### avatar sync on login (PR #685, Dec 31) 751 + 752 + **avatars now stay fresh** - previously set once at artist creation, causing stale/broken avatars throughout the app: 753 + - on login, avatar is refreshed from Bluesky and synced to both postgres and ATProto profile record 754 + - added `avatar` field to `fm.plyr.actor.profile` lexicon (optional, URI format) 755 + - one-time backfill script (`scripts/backfill_avatars.py`) refreshed 28 stale avatars in production 756 + 757 + --- 758 + 759 + ### automated image moderation (PRs #687-690, Dec 31) 760 + 761 + **Claude vision integration** for sensitive image detection: 762 + - images analyzed on upload via Claude Sonnet 4.5 (had to fix model ID - was using wrong identifier) 763 + - flagged images trigger DM notifications to admin 764 + - non-false-positive flags sent to batch review queue 765 + - complements the batch review system built earlier in the sprint 766 + 767 + --- 768 + 769 + ### header redesign (PR #691, Dec 31) 770 + 771 + **new header layout** with UserMenu dropdown and even spacing across the top bar.
+13 -164
STATUS.md
··· 137 138 ### December 2025 139 140 - #### header redesign (PR #691, Dec 31) 141 - 142 - **new header layout** with UserMenu dropdown and even spacing across the top bar. 143 - 144 - --- 145 - 146 - #### automated image moderation (PRs #687-690, Dec 31) 147 - 148 - **Claude vision integration** for sensitive image detection: 149 - - images analyzed on upload via Claude Sonnet 4.5 (had to fix model ID - was using wrong identifier) 150 - - flagged images trigger DM notifications to admin 151 - - non-false-positive flags sent to batch review queue 152 - - complements the batch review system built earlier in the sprint 153 - 154 - --- 155 - 156 - #### avatar sync on login (PR #685, Dec 31) 157 - 158 - **avatars now stay fresh** - previously set once at artist creation, causing stale/broken avatars throughout the app: 159 - - on login, avatar is refreshed from Bluesky and synced to both postgres and ATProto profile record 160 - - added `avatar` field to `fm.plyr.actor.profile` lexicon (optional, URI format) 161 - - one-time backfill script (`scripts/backfill_avatars.py`) refreshed 28 stale avatars in production 162 - 163 - --- 164 - 165 - #### top tracks homepage (PR #684, Dec 31) 166 - 167 - **homepage now shows top tracks** - quick access to popular content for new visitors. 168 - 169 - --- 170 - 171 - #### batch review system (PR #672, Dec 30) 172 - 173 - **moderation batch review UI** - mobile-friendly interface for reviewing flagged content: 174 - - filter by flag status, paginated results 175 - - auto-resolve flags for deleted tracks (PR #681) 176 - - full URL in DM notifications (PR #678) 177 - - required auth flow fix (PR #679) - review page was accessible without login 178 - 179 - --- 180 - 181 - #### CSS design tokens (PRs #662-664, Dec 29-30) 182 - 183 - **design system foundations**: 184 - - border-radius tokens (`--radius-sm`, `--radius-md`, etc.) 185 - - typography scale tokens 186 - - consolidated form styles 187 - - documented in `docs/frontend/design-tokens.md` 188 - 189 - --- 190 - 191 - #### self-hosted redis (PRs #674-675, Dec 30) 192 - 193 - **replaced Upstash with self-hosted Redis on Fly.io** - ~$75/month → ~$4/month: 194 - - Upstash pay-as-you-go was charging per command (37M commands = $75) - discovered when reviewing December costs 195 - - docket's heartbeat mechanism is chatty by design, making pay-per-command pricing unsuitable 196 - - self-hosted Redis on 256MB Fly VMs costs fixed ~$2/month per environment 197 - - deployed `plyr-redis` (prod) and `plyr-redis-stg` (staging) 198 - - added CI workflow for redis deployments on merge 199 - 200 - **no state migration needed** - docket stores ephemeral task queue data, job progress lives in postgres. 201 - 202 - **incident (Dec 30)**: while optimizing redis overhead, a `heartbeat_interval=30s` change broke docket task execution. likes created Dec 29-30 were missing ATProto records. reverted in PR #669, documented in `docs/backend/background-tasks.md`. filed upstream: https://github.com/chrisguidry/docket/issues/267 203 - 204 - --- 205 - 206 - #### supporter-gated content (PR #637, Dec 22-23) 207 - 208 - **atprotofans paywall integration** - artists can now mark tracks as "supporters only": 209 - - tracks with `support_gate` require atprotofans validation before playback 210 - - non-supporters see lock icon and "become a supporter" CTA linking to atprotofans 211 - - artists can always play their own gated tracks 212 - 213 - **backend architecture**: 214 - - audio endpoint validates supporter status via atprotofans API before serving gated content 215 - - HEAD requests return 200/401/402 for pre-flight auth checks (avoids CORS issues with cross-origin redirects) 216 - - gated files stored in private R2 bucket, served via presigned URLs (SigV4 signatures) 217 - - `R2Storage.move_audio()` moves files between public/private buckets when toggling gate 218 - - background task handles bucket migration asynchronously 219 - - ATProto record syncs when toggling gate (updates `supportGate` field and `audioUrl` to point at our endpoint instead of R2) 220 - 221 - **frontend**: 222 - - `playback.svelte.ts` guards queue operations with gated checks BEFORE modifying state 223 - - clicking locked track shows toast with CTA - does NOT interrupt current playback 224 - - portal shows support gate toggle in track edit UI 225 - 226 - **key decision**: gated status is resolved server-side in track listings, not client-side. this means the lock icon appears instantly without additional API calls, and prevents information leakage about which tracks are gated vs which the user simply can't access. 227 - 228 - --- 229 - 230 - #### supporter badges (PR #627, Dec 21-22) 231 - 232 - **phase 1 of atprotofans integration**: 233 - - supporter badge displays on artist pages when logged-in viewer supports the artist 234 - - calls atprotofans `validateSupporter` API directly from frontend (public endpoint) 235 - - badge only shows when viewer is authenticated and not viewing their own profile 236 - 237 - --- 238 - 239 - #### rate limit moderation endpoint (PR #629, Dec 21) 240 - 241 - **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. this was the first real probe of our moderation endpoints, validating the decision to add rate limiting before it became a problem. 242 - 243 - --- 244 - 245 - #### end-of-year sprint (PR #626, Dec 20) 246 - 247 - **focus**: two foundational systems with experimental implementations. 248 - 249 - | track | focus | status | 250 - |-------|-------|--------| 251 - | moderation | consolidate architecture, batch review, Claude vision | shipped | 252 - | atprotofans | supporter validation, content gating | shipped | 253 - 254 - **research docs**: 255 - - [moderation architecture overhaul](docs/research/2025-12-20-moderation-architecture-overhaul.md) 256 - - [atprotofans paywall integration](docs/research/2025-12-20-atprotofans-paywall-integration.md) 257 - 258 - --- 259 - 260 - #### beartype + moderation cleanup (PRs #617-619, Dec 19) 261 - 262 - **runtime type checking** (PR #619): 263 - - enabled beartype runtime type validation across the backend 264 - - catches type errors at runtime instead of silently passing bad data 265 - - test infrastructure improvements: session-scoped TestClient fixture (5x faster tests) 266 - 267 - **moderation cleanup** (PRs #617-618): 268 - - consolidated moderation code, addressing issues #541-543 269 - - `sync_copyright_resolutions` now runs automatically via docket Perpetual task 270 - - removed dead `init_db()` from lifespan (handled by alembic migrations) 271 - 272 - --- 273 - 274 - #### UX polish (PRs #604-607, #613, #615, Dec 16-18) 275 - 276 - **login improvements** (PRs #604, #613): 277 - - login page now uses "internet handle" terminology for clarity 278 - - input normalization: strips `@` and `at://` prefixes automatically 279 - 280 - **artist page fixes** (PR #615): 281 - - track pagination on artist pages now works correctly 282 - - fixed mobile album card overflow 283 - 284 - **mobile + metadata** (PRs #605-607): 285 - - Open Graph tags added to tag detail pages for link previews 286 - - mobile modals now use full screen positioning 287 - - fixed `/tag/` routes in hasPageMetadata check 288 - 289 - --- 290 - 291 - #### offline mode foundation (PRs #610-611, Dec 17) 292 - 293 - **experimental offline playback**: 294 - - storage layer using Cache API for audio bytes + IndexedDB for metadata 295 - - `GET /audio/{file_id}/url` backend endpoint returns direct R2 URLs for client-side caching 296 - - "auto-download liked" toggle in experimental settings section 297 - - Player checks for cached audio before streaming from R2 298 - 299 - --- 300 - 301 - ### Earlier December 2025 302 - 303 See `.status_history/2025-12.md` for detailed history including: 304 - visual customization with custom backgrounds (PRs #595-596, Dec 16) 305 - performance & moderation polish (PRs #586-593, Dec 14-15) 306 - mobile UI polish & background task expansion (PRs #558-572, Dec 10-12) ··· 498 499 --- 500 501 - this is a living document. last updated 2026-01-05.
··· 137 138 ### December 2025 139 140 See `.status_history/2025-12.md` for detailed history including: 141 + - header redesign and UI polish (PRs #691-693, Dec 31) 142 + - automated image moderation with Claude vision (PRs #687-690, Dec 31) 143 + - avatar sync on login (PR #685, Dec 31) 144 + - top tracks homepage (PR #684, Dec 31) 145 + - batch review system (PR #672, Dec 30) 146 + - CSS design tokens (PRs #662-664, Dec 29-30) 147 + - self-hosted redis migration (PRs #674-675, Dec 30) 148 + - supporter-gated content (PR #637, Dec 22-23) 149 + - supporter badges (PR #627, Dec 21-22) 150 + - end-of-year sprint: moderation + atprotofans (PRs #617-629, Dec 19-21) 151 + - offline mode foundation (PRs #610-611, Dec 17) 152 + - UX polish and login improvements (PRs #604-615, Dec 16-18) 153 - visual customization with custom backgrounds (PRs #595-596, Dec 16) 154 - performance & moderation polish (PRs #586-593, Dec 14-15) 155 - mobile UI polish & background task expansion (PRs #558-572, Dec 10-12) ··· 347 348 --- 349 350 + this is a living document. last updated 2026-01-06.
update.wav

This is a binary file and will not be displayed.