+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
+17
-177
STATUS.md
+17
-177
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
-
303
140
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)
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)
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)
316
157
- unified search with Cmd+K (PR #447, Dec 3)
317
158
- light/dark theme system (PR #441, Dec 2-3)
318
-
- tag filtering and bufo easter egg (PRs #431-438, Dec 2)
319
159
320
160
### November 2025
321
161
···
498
338
499
339
---
500
340
501
-
this is a living document. last updated 2026-01-05.
341
+
this is a living document. last updated 2026-01-06.
update.wav
update.wav
This is a binary file and will not be displayed.