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 47 48 48 ### January 2026 49 49 50 - #### copyright moderation improvements (PRs #703-704, Jan 2) 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) 51 92 52 93 **per legal advice**, redesigned copyright handling to reduce liability exposure: 53 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 ··· 96 137 97 138 ### December 2025 98 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 + 99 156 #### avatar sync on login (PR #685, Dec 31) 100 157 101 158 **avatars now stay fresh** - previously set once at artist creation, causing stale/broken avatars throughout the app: ··· 105 162 106 163 --- 107 164 108 - #### self-hosted redis (PR #674-675, Dec 30) 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) 109 192 110 193 **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) 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 112 196 - self-hosted Redis on 256MB Fly VMs costs fixed ~$2/month per environment 113 197 - deployed `plyr-redis` (prod) and `plyr-redis-stg` (staging) 114 198 - added CI workflow for redis deployments on merge 115 199 116 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 117 203 118 204 --- 119 205 ··· 126 212 127 213 **backend architecture**: 128 214 - 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) 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) 130 217 - `R2Storage.move_audio()` moves files between public/private buckets when toggling gate 131 218 - background task handles bucket migration asynchronously 132 - - ATProto record syncs when toggling gate (updates `supportGate` field and `audioUrl`) 219 + - ATProto record syncs when toggling gate (updates `supportGate` field and `audioUrl` to point at our endpoint instead of R2) 133 220 134 221 **frontend**: 135 222 - `playback.svelte.ts` guards queue operations with gated checks BEFORE modifying state 136 223 - clicking locked track shows toast with CTA - does NOT interrupt current playback 137 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. 138 227 139 228 --- 140 229 ··· 149 238 150 239 #### rate limit moderation endpoint (PR #629, Dec 21) 151 240 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. 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. 153 242 154 243 --- 155 244 156 - #### end-of-year sprint planning (PR #626, Dec 20) 245 + #### end-of-year sprint (PR #626, Dec 20) 157 246 158 - **focus**: two foundational systems need solid experimental implementations by 2026. 247 + **focus**: two foundational systems with experimental implementations. 159 248 160 249 | track | focus | status | 161 250 |-------|-------|--------| 162 - | moderation | consolidate architecture, add rules engine | in progress | 163 - | atprotofans | supporter validation, content gating | shipped (phase 1-3) | 251 + | moderation | consolidate architecture, batch review, Claude vision | shipped | 252 + | atprotofans | supporter validation, content gating | shipped | 164 253 165 254 **research docs**: 166 255 - [moderation architecture overhaul](docs/research/2025-12-20-moderation-architecture-overhaul.md) ··· 236 325 - export & upload reliability (PRs #337-344) 237 326 - transcoder API deployment (PR #156) 238 327 239 - ## immediate priorities 328 + ## priorities 240 329 241 - ### quality of life mode (Dec 29-31) 330 + ### current focus 242 331 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. 332 + stabilization and polish after multi-account release. monitoring production for issues. 244 333 245 - **what shipped in the sprint:** 334 + **end-of-year sprint [#625](https://github.com/zzstoatzz/plyr.fm/issues/625) shipped:** 246 335 - moderation consolidation: sensitive images moved to moderation service (#644) 336 + - moderation batch review UI with Claude vision integration (#672, #687-690) 247 337 - atprotofans: supporter badges (#627) and content gating (#637) 248 338 249 - **aspirational (deferred until scale justifies):** 250 - - configurable rules engine for moderation 251 - - time-release gating (#642) 252 - 253 339 ### known issues 254 340 - playback auto-start on refresh (#225) 255 341 - iOS PWA audio may hang on first play after backgrounding ··· 258 344 - audio transcoding pipeline integration (#153) - transcoder service deployed, integration deferred 259 345 - share to bluesky (#334) 260 346 - lyrics and annotations (#373) 347 + - configurable rules engine for moderation 348 + - time-release gating (#642) 261 349 262 350 ## technical state 263 351 ··· 290 378 291 379 **core functionality** 292 380 - ✅ ATProto OAuth 2.1 authentication 381 + - ✅ multi-account support (link multiple Bluesky accounts) 293 382 - ✅ secure session management via HttpOnly cookies 294 383 - ✅ developer tokens with independent OAuth grants 295 384 - ✅ platform stats and Media Session API ··· 409 498 410 499 --- 411 500 412 - this is a living document. last updated 2026-01-02. 501 + this is a living document. last updated 2026-01-05.