-171
.status_history/2025-12.md
-171
.status_history/2025-12.md
···
606
606
**documentation** (PR #514):
607
607
- added lexicons overview documentation at `docs/lexicons/overview.md`
608
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.
772
-
773
-
---
774
-
775
-
### UI polish (PRs #692-694, Dec 31 - Jan 1)
776
-
777
-
- **feed/library toggle** (PR #692): consistent header layout with toggle between feed and library views
778
-
- **shuffle button moved** (PR #693): shuffle now in queue component instead of player controls
779
-
- **justfile consistency** (PR #694): standardized `just run` across frontend/backend modules
+177
-17
STATUS.md
+177
-17
STATUS.md
···
137
137
138
138
### December 2025
139
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
+
140
303
See `.status_history/2025-12.md` for detailed history including:
141
-
- header redesign and UI polish (PRs #691-694, Dec 31)
142
-
- automated image moderation via 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 (PR #626, Dec 20)
151
-
- offline mode foundation (PRs #610-611, Dec 17)
152
-
- UX polish and login improvements (PRs #604-615, Dec 16-18)
153
-
- visual customization (PRs #595-596, Dec 16)
154
-
- confidential OAuth client (PRs #578-582, Dec 12-13)
155
-
- docket background tasks (PRs #534-546, Dec 9)
156
-
- playlists and library hub (PR #499, Dec 6-7)
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)
307
+
- confidential OAuth client for 180-day sessions (PRs #578-582, Dec 12-13)
308
+
- pagination & album management (PRs #550-554, Dec 9-10)
309
+
- public cost dashboard (PRs #548-549, Dec 9)
310
+
- docket background tasks & concurrent exports (PRs #534-546, Dec 9)
311
+
- artist support links & inline playlist editing (PRs #520-532, Dec 8)
312
+
- playlist fast-follow fixes (PRs #507-519, Dec 7-8)
313
+
- playlists, ATProto sync, and library hub (PR #499, Dec 6-7)
314
+
- sensitive image moderation (PRs #471-488, Dec 5-6)
315
+
- teal.fm scrobbling (PR #467, Dec 4)
157
316
- unified search with Cmd+K (PR #447, Dec 3)
158
317
- light/dark theme system (PR #441, Dec 2-3)
318
+
- tag filtering and bufo easter egg (PRs #431-438, Dec 2)
159
319
160
320
### November 2025
161
321
···
338
498
339
499
---
340
500
341
-
this is a living document. last updated 2026-01-06.
501
+
this is a living document. last updated 2026-01-05.
update.wav
update.wav
This is a binary file and will not be displayed.