docs: update STATUS.md with January work and fix stale sections (#717)

- add multi-account experience (PRs #707, #710, #712-714)
- add artist bio links (PRs #700-701)
- add missing Dec 30-31 work (header redesign, Claude image moderation, batch review, design tokens, top tracks)
- fix end-of-year sprint table: moderation now shows 'shipped' not 'in progress'
- update priorities section with sprint completion summary
- add multi-account to 'what's working' list
- update last modified date

authored by zzstoatzz.io and committed by GitHub a8708fff e501351c

Changed files
+108 -19
+108 -19
STATUS.md
··· 47 48 ### January 2026 49 50 - #### copyright moderation improvements (PRs #703-704, Jan 2) 51 52 **per legal advice**, redesigned copyright handling to reduce liability exposure: 53 - **disabled auto-labeling** (PR #703): labels are no longer automatically emitted when copyright matches are detected. the system now only flags and notifies, leaving takedown decisions to humans ··· 96 97 ### December 2025 98 99 #### avatar sync on login (PR #685, Dec 31) 100 101 **avatars now stay fresh** - previously set once at artist creation, causing stale/broken avatars throughout the app: ··· 105 106 --- 107 108 - #### self-hosted redis (PR #674-675, Dec 30) 109 110 **replaced Upstash with self-hosted Redis on Fly.io** - ~$75/month → ~$4/month: 111 - - Upstash pay-as-you-go was charging per command (37M commands = $75) 112 - self-hosted Redis on 256MB Fly VMs costs fixed ~$2/month per environment 113 - deployed `plyr-redis` (prod) and `plyr-redis-stg` (staging) 114 - added CI workflow for redis deployments on merge 115 116 **no state migration needed** - docket stores ephemeral task queue data, job progress lives in postgres. 117 118 --- 119 ··· 126 127 **backend architecture**: 128 - audio endpoint validates supporter status via atprotofans API before serving gated content 129 - - HEAD requests return 200/401/402 for pre-flight auth checks (avoids CORS issues) 130 - `R2Storage.move_audio()` moves files between public/private buckets when toggling gate 131 - background task handles bucket migration asynchronously 132 - - ATProto record syncs when toggling gate (updates `supportGate` field and `audioUrl`) 133 134 **frontend**: 135 - `playback.svelte.ts` guards queue operations with gated checks BEFORE modifying state 136 - clicking locked track shows toast with CTA - does NOT interrupt current playback 137 - portal shows support gate toggle in track edit UI 138 139 --- 140 ··· 149 150 #### rate limit moderation endpoint (PR #629, Dec 21) 151 152 - **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. 153 154 --- 155 156 - #### end-of-year sprint planning (PR #626, Dec 20) 157 158 - **focus**: two foundational systems need solid experimental implementations by 2026. 159 160 | track | focus | status | 161 |-------|-------|--------| 162 - | moderation | consolidate architecture, add rules engine | in progress | 163 - | atprotofans | supporter validation, content gating | shipped (phase 1-3) | 164 165 **research docs**: 166 - [moderation architecture overhaul](docs/research/2025-12-20-moderation-architecture-overhaul.md) ··· 236 - export & upload reliability (PRs #337-344) 237 - transcoder API deployment (PR #156) 238 239 - ## immediate priorities 240 241 - ### quality of life mode (Dec 29-31) 242 243 - end-of-year sprint [#625](https://github.com/zzstoatzz/plyr.fm/issues/625) complete. remaining days before 2026 are for minor polish and bug fixes as they arise. 244 245 - **what shipped in the sprint:** 246 - moderation consolidation: sensitive images moved to moderation service (#644) 247 - atprotofans: supporter badges (#627) and content gating (#637) 248 249 - **aspirational (deferred until scale justifies):** 250 - - configurable rules engine for moderation 251 - - time-release gating (#642) 252 - 253 ### known issues 254 - playback auto-start on refresh (#225) 255 - iOS PWA audio may hang on first play after backgrounding ··· 258 - audio transcoding pipeline integration (#153) - transcoder service deployed, integration deferred 259 - share to bluesky (#334) 260 - lyrics and annotations (#373) 261 262 ## technical state 263 ··· 290 291 **core functionality** 292 - ✅ ATProto OAuth 2.1 authentication 293 - ✅ secure session management via HttpOnly cookies 294 - ✅ developer tokens with independent OAuth grants 295 - ✅ platform stats and Media Session API ··· 409 410 --- 411 412 - this is a living document. last updated 2026-01-02.
··· 47 48 ### January 2026 49 50 + #### multi-account experience (PRs #707, #710, #712-714, Jan 3-5) 51 + 52 + **why**: many users have multiple Bluesky identities (personal, artist, label). forcing re-authentication to switch was friction that discouraged uploads from secondary accounts. 53 + 54 + **users can now link multiple Bluesky accounts** to a single browser session: 55 + - add additional accounts via "add account" in user menu (triggers OAuth with `prompt=login`) 56 + - switch between linked accounts instantly without re-authenticating 57 + - logout from individual accounts or all at once 58 + - updated `/auth/me` returns `linked_accounts` array with avatars 59 + 60 + **backend changes**: 61 + - new `group_id` column on `user_sessions` links accounts together 62 + - new `pending_add_accounts` table tracks in-progress OAuth flows 63 + - new endpoints: `POST /auth/add-account/start`, `POST /auth/switch-account`, `POST /auth/logout-all` 64 + 65 + **infrastructure fixes** (PRs #710, #712, #714): 66 + these fixes came from reviewing [Bluesky's architecture deep dive](https://newsletter.pragmaticengineer.com/p/bluesky) which highlighted connection/resource management as scaling concerns. applied learnings to our own codebase: 67 + - identified Neon serverless connection overhead (~77ms per connection) via Logfire 68 + - cached `async_sessionmaker` per engine instead of recreating on every request (PR #712) 69 + - changed `_refresh_locks` from unbounded dict to LRUCache (10k max, 1hr TTL) to prevent memory leak (PR #710) 70 + - pass db session through auth helpers to reduce connections per request (PR #714) 71 + - result: `/auth/switch-account` ~1100ms → ~800ms, `/auth/me` ~940ms → ~720ms 72 + 73 + **frontend changes**: 74 + - UserMenu (desktop): collapsible accounts submenu with linked accounts, add account, logout all 75 + - ProfileMenu (mobile): dedicated accounts panel with avatars 76 + - fixed `invalidateAll()` not refreshing client-side loaded data by using `window.location.reload()` (PR #713) 77 + 78 + **docs**: [research/2026-01-03-multi-account-experience.md](docs/research/2026-01-03-multi-account-experience.md) 79 + 80 + --- 81 + 82 + #### artist bio links (PRs #700-701, Jan 2) 83 + 84 + **links in artist bios now render as clickable** - supports full URLs and bare domains (e.g., "example.com"): 85 + - regex extracts URLs from bio text 86 + - bare domain/path URLs handled correctly 87 + - links open in new tab 88 + 89 + --- 90 + 91 + #### copyright moderation improvements (PRs #703-704, Jan 2-3) 92 93 **per legal advice**, redesigned copyright handling to reduce liability exposure: 94 - **disabled auto-labeling** (PR #703): labels are no longer automatically emitted when copyright matches are detected. the system now only flags and notifies, leaving takedown decisions to humans ··· 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: ··· 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 ··· 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 ··· 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) ··· 325 - export & upload reliability (PRs #337-344) 326 - transcoder API deployment (PR #156) 327 328 + ## priorities 329 330 + ### current focus 331 332 + stabilization and polish after multi-account release. monitoring production for issues. 333 334 + **end-of-year sprint [#625](https://github.com/zzstoatzz/plyr.fm/issues/625) shipped:** 335 - moderation consolidation: sensitive images moved to moderation service (#644) 336 + - moderation batch review UI with Claude vision integration (#672, #687-690) 337 - atprotofans: supporter badges (#627) and content gating (#637) 338 339 ### known issues 340 - playback auto-start on refresh (#225) 341 - iOS PWA audio may hang on first play after backgrounding ··· 344 - audio transcoding pipeline integration (#153) - transcoder service deployed, integration deferred 345 - share to bluesky (#334) 346 - lyrics and annotations (#373) 347 + - configurable rules engine for moderation 348 + - time-release gating (#642) 349 350 ## technical state 351 ··· 378 379 **core functionality** 380 - ✅ ATProto OAuth 2.1 authentication 381 + - ✅ multi-account support (link multiple Bluesky accounts) 382 - ✅ secure session management via HttpOnly cookies 383 - ✅ developer tokens with independent OAuth grants 384 - ✅ platform stats and Media Session API ··· 498 499 --- 500 501 + this is a living document. last updated 2026-01-05.