commits
- add backend support for removing track artwork via `remove_image` form field
- show image preview when selecting new artwork before saving
- add ability to remove existing artwork (not just replace)
- replace small icon-only buttons with labeled pill buttons
- improve edit mode save/cancel buttons with subtle outlined styling
- add mobile-responsive layout for artwork editor
- fix shutdown hangs: add 2s timeouts to docket worker and service shutdown
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
beartype sees fastapi.UploadFile and starlette.UploadFile as different
classes even though FastAPI re-exports starlette's class. When FastAPI
parses the multipart form, it creates starlette.UploadFile instances,
but our type hint said fastapi.UploadFile - causing BeartypeCallHintParamViolation.
Also switched to logfire.exception() for better error visibility.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
add optional autoPlay param to queue.playNow() to allow loading
a track without triggering auto-play. used for ?t= timestamp URLs
where browser policy blocks autoplay without user interaction.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: timestamped comment sharing
add youtube-style ?t= param support for deep linking to specific timestamps:
- parse ?t=X on page load and auto-seek to X seconds
- add "link" button to all comments that copies timestamped URL
- existing OpenGraph meta tags already handle link previews
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: use onMount for timestamp param, rename link to share
* fix: use reactive effect for timestamp seek, document pattern
* fix: ensure playback continues after seek
* fix: don't force autoplay on timestamp links
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
chore: improve status-update workflow and document auth stabilization
Updates the /status-update slash command with a structured workflow:
1. Read current STATUS.md state
2. Find undocumented work via git history
3. Understand WHY changes were made (not just what)
4. Write contextual documentation for future readers
Documents recent auth stabilization work (PRs #734-736):
- Session expiry now respects refresh token lifetime
- Queue component uses shared auth state (SSR boundary fix)
- BytesIO accepted in R2Storage.save() (playlist cover fix)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
fix: accept BytesIO in R2Storage.save() type hint
beartype was rejecting BytesIO objects passed to R2Storage.save()
because the BinaryIO type hint doesn't satisfy beartype's strict
protocol checking for BytesIO instances.
Changed type hint from `BinaryIO` to `BinaryIO | BytesIO` to
explicitly accept both types. This fixes playlist cover uploads
which use BytesIO to wrap image data.
Adds regression test that verifies BytesIO is accepted.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Align queue auth with layout state
Align session expiry with refresh token lifetime
covers:
- how the workflow determines time windows
- archival rules and line count targets
- audio generation (pronunciation, terminology, tone)
- troubleshooting common issues (reverted PRs, wrong terminology)
also fixes: tag format from '["ai"]' to "ai"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
🤖 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>
STATUS.md was incorrectly referring to "Bluesky accounts" when it should
say "ATProto identities". plyr.fm operates at the protocol layer, not
the application layer.
- "multiple Bluesky identities" → "multiple ATProto identities"
- "link multiple Bluesky accounts" → "link multiple identities"
- added terminology guidance to status maintenance prompt
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This reverts commit 53cebb1e985282fd6d287771851659cf8b8b84a5.
🤖 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>
the previous prompt was ambiguous about what "archiving" meant, leading
to PR #719 condensing cross-references instead of actually moving
content to archive files.
changes:
- explicit line count targets (ideal ~200, acceptable 300-450, max 500)
- clear rule: archive content from PREVIOUS months, keep current month
- step-by-step instructions for CUT/PASTE workflow
- emphasize "archiving = moving content" not "summarizing in place"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Users were getting confused with no way to navigate back.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The album endpoint was showing ~205ms average but HTTP Request spans
to PDS were showing 0ms duration - an instrumentation gap.
This adds a surgical `logfire.span()` wrapper around `get_record_public()`
to capture actual PDS HTTP call duration without flooding spans like
global httpx instrumentation would.
Span attributes:
- collection: the ATProto collection being fetched
- rkey: the record key
- pds_host: the PDS hostname
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- 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
each db_session() creates a new Neon connection (~77ms overhead).
the multi-account endpoints were creating 3 separate connections:
1. require_auth -> get_session()
2. get_session_group()
3. switch_active_account() or artist lookup
now get_session_group() and switch_active_account() accept optional
db parameter to reuse an existing connection. endpoints pass their
injected db through, reducing connection overhead by ~154ms.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
invalidateAll() only re-runs SvelteKit load functions, but the portal
page loads data client-side in onMount. switching accounts left stale
data on the page. using window.location.reload() ensures fresh data.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* docs: add multi-account experience design spec
research document exploring multi-account UX for users with multiple
ATProto identities. covers OAuth prompt parameter support, session
groups architecture, and phased implementation plan.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* docs: add bluesky implementation study to multi-account spec
studied bluesky's open-source social-app to inform our design:
- session state patterns (accounts array, currentAccount reference)
- UX patterns (avatars, checkmarks, "logged out" labels)
- logout distinction (active only vs all)
- cross-tab sync approach
also updated prerequisite section with link to SDK fork PR #8.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore: update atproto SDK with prompt parameter support
updates to d4830f4 which adds PromptType and prompt parameter
to start_authorization() for multi-account flows.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* docs: expand bluesky implementation study with detailed patterns
- added persistence layer details (AsyncStorage, schema structure)
- documented full SessionAccount interface with all fields
- listed reducer action types for state transitions
- added token refresh strategy comparison
- documented AccountList UI implementation details
- expanded references with direct links to key source files
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: implement multi-account experience
backend:
- add session groups (group_id, is_active, avatar_url columns)
- add pending_add_accounts table for OAuth flow tracking
- add /auth/add-account/start endpoint with prompt=login
- add /auth/switch-account endpoint to switch active session
- add /auth/logout-all endpoint to clear all linked accounts
- update /auth/me to return linked_accounts list
- update callback to handle add-account flow
frontend:
- add LinkedAccount type and update User type
- update UserMenu with account switcher dropdown
- update ProfileMenu with accounts sub-menu for mobile
- show avatars, handles, and switch/add/logout options
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: add-account flow now prompts for new handle
the add-account endpoint was incorrectly using the current user's handle,
which locked the OAuth flow to the same account. now:
- backend: /auth/add-account/start requires a handle in the request body
- backend: validates that handle differs from current account
- frontend: shows inline handle input before starting OAuth flow
- frontend: same UX in UserMenu (desktop) and ProfileMenu (mobile)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: stop propagation on add account click
the click outside handler was closing the menu before the input could
appear because the button was being removed from DOM, causing
menuRef.contains(event.target) to return false.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: deactivate other sessions when adding new account
when a new account is added to a session group, only the new session
should be marked as active. the frontend filters for !is_active to show
switchable accounts, so both being active meant neither showed up.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: filter other accounts by DID, not is_active flag
the cookie determines which session is active - we don't need a separate
is_active flag. just filter out the current user's DID to show other
accounts in the switch list.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: remove unnecessary is_active tracking
the cookie determines which session is active - tracking is_active
separately was redundant complexity.
- removed deactivate_other_sessions_in_group function
- simplified switch_active_account to just validate and return session_id
- switch-account endpoint now checks DID instead of is_active flag
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: remove is_active from API response and types
is_active was never needed - the cookie determines the active session,
and we filter by DID in the frontend. removed from:
- LinkedAccountResponse model
- LinkedAccount TypeScript interface
- handleSwitchAccount checks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: accounts submenu with avatars and logout-all fix
- backend: look up artist avatars in /auth/me for fresh data
- UserMenu: consolidate accounts into collapsible submenu
- ProfileMenu: show current user avatar
- fix: use window.location.href for logout-all to clear state
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: improve multi-account UX
- use invalidateAll() instead of page reload when switching accounts
(prevents "log in" flash during account switch)
- add logout prompt for multi-account users to stay logged in as
another account instead of fully logging out
- add switch_to param to /logout for atomic logout + switch
- fix add-account validation to check ALL accounts in session group
- fix dropdown width (220px) to prevent horizontal expansion
- use HandleAutocomplete in add-account forms
- fix a11y warning in portal page (label → span)
- remove unused .desktop-nav CSS
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: prevent logout button click from closing menu before prompt shows
when clicking logout with multiple accounts, the DOM update that shows
the logout prompt was removing the logout button from the DOM before
the click-outside handler checked containment, causing the menu to close.
added event.stopPropagation() to handleLogoutClick in both UserMenu and
ProfileMenu to prevent the click from bubbling to the document listener.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: convert logout prompt to centered modal
- Create global LogoutState class using Svelte 5 runes
- Add LogoutModal component rendered at root layout level
- Update UserMenu and ProfileMenu to use global logout state
- Modal escapes header's backdrop-filter containing block
- Fix click-outside race condition with stopPropagation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test: add multi-account session management tests
covers session groups, account switching, removal, and pending add-account flow.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: remove is_active and avatar_url from session model
these columns were adding complexity without value:
- is_active: cookie is the source of truth for active session
- avatar_url: /auth/me already fetches fresh from Artist table
simplifies remove_account_from_group to just return first remaining session.
removes unused update_session_avatar function.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* migrate: drop is_active and avatar_url from user_sessions
these columns were added to dev database in earlier development
but the model was simplified before merge. this migration aligns
the database schema with the final model.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: make column drop migration idempotent
the columns were only added to dev database during development.
staging and prod never had them. this migration now checks if
columns exist before attempting to drop them.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
fix: cache sessionmaker per engine to avoid recreation overhead
fix: prevent unbounded memory growth in token refresh locks
- use underscore separator for readability in large integer
- move cryptography imports to module level
- remove unnecessary comments
- import _refresh_locks directly instead of inside each test
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
TTLCache could expire a lock while a coroutine holds a reference to it,
breaking mutual exclusion. LRUCache evicts by recency instead, so locks
actively in use won't be evicted.
Also adds tests verifying:
- same session_id returns same lock
- different sessions have different locks
- cache is bounded by maxsize
- LRU eviction order
Related to #708
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
use async_sessionmaker (SQLAlchemy 2.0) and cache it alongside the engine
instead of recreating it on every db_session() call. while the overhead
is minimal, this follows the intended pattern and reduces object churn.
changes:
- switch from sessionmaker to async_sessionmaker (proper async API)
- add SESSION_MAKERS cache dict alongside ENGINES
- add get_session_maker() function for cached retrieval
Related to #708
use TTLCache instead of plain dict for _refresh_locks to auto-expire
locks for inactive sessions. prevents memory leak as sessions are
created and abandoned over time.
- max 10k concurrent sessions cached
- 1 hour TTL auto-expires unused locks
- uses existing cachetools dependency
Related to #708
feat: send DM notifications when tracks are copyright flagged
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- refactor send_track_notification to use _send_dm_to_did
- refactor send_image_flag_notification to use _send_dm_to_did
- all notification methods now have consistent error handling and logfire spans
- reduces code duplication (~50 lines)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- add NotificationResult dataclass with success, error, error_type fields
- categorize errors: dm_blocked, network, auth, unknown
- add logfire spans to _send_dm_to_did and send_copyright_notification
- log warnings with structured data when notifications fail
- return tuple of results so caller knows exactly what happened
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- add send_copyright_notification() to NotificationService
- DM both the artist (uploader) and admin when a track is flagged
- add notified_at field to CopyrightScan model to track notification status
- artist message explains the flag and invites dispute
- admin message includes match details and track link
related to #702
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
changes:
- remove auto-label emission from _store_scan_result (was creating liability)
- add MODERATION_COPYRIGHT_SCORE_THRESHOLD env var (default: 85)
- remove tests that expected auto-label emission
scanning still runs and stores results internally for future use
when we build the notification + action pipeline.
refs #702
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
adds support for github.com/... style URLs without protocol prefix.
regex now matches domain.tld/path patterns and prepends https://.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: render links in artist bio
Adds RichText component that auto-detects and renders:
- bare URLs (https://example.com, www.example.com)
- markdown-style links ([text](url))
Links open in new tab with proper security attributes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: align link hover style with existing patterns
remove opacity transition to match .comment-link styling.
add word-break: break-all for long URLs.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Updates the atproto dependency to include the fix for OAuth scope
validation when using permission sets. The PDS expands `include:`
scopes into `repo?collection=` format, which the SDK now handles.
Also updates docs with correct DNS setup (`_lexicon` prefix, not `_atproto`).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
adds permission set lexicon that bundles OAuth permissions under
a human-readable title. users will see "plyr.fm Music Library" instead
of "fm.plyr.track, fm.plyr.like, fm.plyr.comment..."
lexicon:
- fm.plyr.authFullApp: full access for main web app
config:
- ATPROTO_USE_PERMISSION_SETS=true enables permission sets
- defaults to false (granular scopes) until lexicons are published
docs:
- research doc on how permission sets work
- updated lexicons overview with permission set section
to enable: publish lexicon to com.atproto.lexicon.schema on plyr.fm
authority repo, then set ATPROTO_USE_PERMISSION_SETS=true
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
FastAPI was matching /artists/batch as /{did} with did="batch".
Moving the POST /batch route before the GET /{did} route fixes this.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: show atprotofans supporter count and list on artist pages
- add `getAtprotofansProfile` and `getAtprotofansSupporters` API calls
- display supporter count badge in support button
- add supporters section with avatar grid linking to bsky profiles
- responsive styling for mobile
uses atprotofans public endpoints:
- `com.atprotofans.getProfile` for supporter count
- `com.atprotofans.getSupporters` for list of supporters
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: remove count badge from support button, fix avatar handling
- remove supporter count from support button (unnecessary)
- properly check avatar field before rendering
- clean up unused CSS
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: enrich atprotofans supporters with avatars from Bluesky
Fetch each supporter's profile from Bluesky's public API
(app.bsky.actor.getProfile) to get their avatar URL, following
the same pattern used elsewhere in the app where backend
provides avatar_url for display.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: remove frontend Bluesky API calls for avatar enrichment
Don't add external API surface area from the frontend - use what
atprotofans returns directly. If avatars aren't provided, the
placeholder will show. Backend should handle enrichment if needed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: enrich supporters with avatar_url via backend batch endpoint
- Add POST /artists/batch endpoint to get artist data for multiple DIDs
- Frontend calls atprotofans for supporter DIDs, then enriches via our backend
- Uses same pattern as likers: avatar_url comes from Artist table
- Use SensitiveImage wrapper and initials placeholder (consistent with LikersTooltip)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* style: compact overlapping avatar circles for supporters
GitHub-sponsors style: small circles with negative margin overlap,
"+N" badge for overflow. Bounded height regardless of supporter count.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: link supporter avatars to plyr.fm artist pages
Keep users in the app instead of linking to Bluesky.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- frontend: rename `dev` to `run` (keep `dev` as alias for compat)
- add `r` alias to backend and frontend (matches transcoder/moderation)
- add `just tunnel` command for ngrok
- update docs to reference `just frontend run`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Removes shuffle button from PlaybackControls
- Adds shuffle button to Queue header next to clear button
- Same functionality, just relocated to reduce player clutter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add feed/library toggle to maintain consistent header items
When on library page, shows "feed" button to go home.
When not on library, shows "library" button.
This keeps the same number of nav items regardless of page,
preventing layout shifts from space-between redistribution.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: include liked and playlist pages in collection check
Shows feed button on /library, /liked, and /playlist/* pages.
Shows library button everywhere else.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: show feed/library buttons independently
- Feed button: shows when not on homepage
- Library button: shows when not on /library
- Both can show at the same time, space-between handles it
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: redesign header for cleaner desktop layout
- add UserMenu dropdown (handle + portal/settings/logout)
- simplify desktop nav: search | library | upload | user menu
- move social links and stats to LinksMenu only (mobile)
- remove unused SearchTrigger and SettingsMenu components
- update CLAUDE.md to reflect component changes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* style: increase nav item spacing for better visual rhythm
* style: flatten header structure for even space-between distribution
* fix: restore social links (bluesky, status, tangled) to desktop header
* fix: move social links to left of logo (original position)
* feat: add social links to header left margin
Restores Bluesky, status page, and Tangled links in absolutely
positioned left margin area, outside the main header content flow.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: hide social links on narrow desktop screens
Adds media query to hide margin-left when viewport is under 1000px,
preventing overflow when there's insufficient margin space.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: remove nav spacers for proper redistribution
Nav items now naturally redistribute when library/upload links are
hidden on their respective pages, instead of leaving awkward gaps.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When Claude flags an uploaded image (track cover or album cover) as
sensitive, send a DM to the admin with details about the flag.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update model from claude-3-5-sonnet-latest to claude-sonnet-4-5-20250929.
Tested and verified working with production deployment.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
adds automated scanning of uploaded images (track covers, album covers)
for policy violations using Claude Sonnet vision capabilities.
## moderation service (rust)
- new `/scan-image` endpoint accepts multipart form with image + image_id
- `claude.rs`: Claude API client with vision support
- `image_scans` table for cost tracking and audit trail
- auto-flags unsafe images to `sensitive_images` table
## backend (python)
- `ModerationClient.scan_image()` method for calling the new endpoint
- integration in `upload_track_image()` and album cover upload
- `image_moderation_enabled` setting (default: true)
moderation is best-effort - failures are logged but don't block uploads.
flagged images are blurred in the UI (existing sensitive_images behavior).
closes #166
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: sync avatar on login and add to ATProto profile record
- add optional avatar field to fm.plyr.actor.profile lexicon
- update profile record builder to accept and include avatar
- refresh avatar from bluesky on login sync
- update postgres if avatar changed
- sync avatar to ATProto profile record
fixes stale avatars in likers tooltip and other displays
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* add one-time avatar backfill script
refreshes avatar_url for all artists from bluesky. run with:
cd backend && uv run python ../scripts/backfill_avatars.py --dry-run
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
adds a "top tracks" section above the existing "latest tracks" feed,
showing the 10 most-liked tracks on the platform. this helps surface
quality content instead of the homepage being dominated by bulk uploads.
backend:
- new `/tracks/top` endpoint returning tracks ordered by like count
- `get_top_track_ids()` aggregation helper
frontend:
- fetches top tracks concurrently with latest tracks on mount
- displays section only when there are liked tracks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
previously VIOLATION flags were ignored and left in pending limbo.
now anything that isn't auto-resolved as FALSE_POSITIVE goes to the
batch for human review.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
displays count matching current filter (e.g., "27 pending", "5 resolved")
right-aligned in the filter row for quick visibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- add PlyrClient to check track existence via plyr.fm API
- before LLM analysis, check each flagged track still exists
- auto-resolve with reason "content_deleted" if track returns 404
- add ContentDeleted variant to ResolutionReason enum in Rust
prevents labels from persisting in the ether for deleted content
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- move review to /admin/review/:id (proper admin namespace)
- use admin.css for consistent styling with dashboard
- add "← back to dashboard" navigation link
- add three action types:
- clear: false positive, emit negation label
- defer: acknowledge but take no action (flag stays active)
- confirm: mark as real violation (flag stays active)
- toggle decisions by clicking same button again
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- add backend support for removing track artwork via `remove_image` form field
- show image preview when selecting new artwork before saving
- add ability to remove existing artwork (not just replace)
- replace small icon-only buttons with labeled pill buttons
- improve edit mode save/cancel buttons with subtle outlined styling
- add mobile-responsive layout for artwork editor
- fix shutdown hangs: add 2s timeouts to docket worker and service shutdown
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
beartype sees fastapi.UploadFile and starlette.UploadFile as different
classes even though FastAPI re-exports starlette's class. When FastAPI
parses the multipart form, it creates starlette.UploadFile instances,
but our type hint said fastapi.UploadFile - causing BeartypeCallHintParamViolation.
Also switched to logfire.exception() for better error visibility.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
add optional autoPlay param to queue.playNow() to allow loading
a track without triggering auto-play. used for ?t= timestamp URLs
where browser policy blocks autoplay without user interaction.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: timestamped comment sharing
add youtube-style ?t= param support for deep linking to specific timestamps:
- parse ?t=X on page load and auto-seek to X seconds
- add "link" button to all comments that copies timestamped URL
- existing OpenGraph meta tags already handle link previews
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: use onMount for timestamp param, rename link to share
* fix: use reactive effect for timestamp seek, document pattern
* fix: ensure playback continues after seek
* fix: don't force autoplay on timestamp links
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Updates the /status-update slash command with a structured workflow:
1. Read current STATUS.md state
2. Find undocumented work via git history
3. Understand WHY changes were made (not just what)
4. Write contextual documentation for future readers
Documents recent auth stabilization work (PRs #734-736):
- Session expiry now respects refresh token lifetime
- Queue component uses shared auth state (SSR boundary fix)
- BytesIO accepted in R2Storage.save() (playlist cover fix)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
fix: accept BytesIO in R2Storage.save() type hint
beartype was rejecting BytesIO objects passed to R2Storage.save()
because the BinaryIO type hint doesn't satisfy beartype's strict
protocol checking for BytesIO instances.
Changed type hint from `BinaryIO` to `BinaryIO | BytesIO` to
explicitly accept both types. This fixes playlist cover uploads
which use BytesIO to wrap image data.
Adds regression test that verifies BytesIO is accepted.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Align queue auth with layout state
covers:
- how the workflow determines time windows
- archival rules and line count targets
- audio generation (pronunciation, terminology, tone)
- troubleshooting common issues (reverted PRs, wrong terminology)
also fixes: tag format from '["ai"]' to "ai"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
STATUS.md was incorrectly referring to "Bluesky accounts" when it should
say "ATProto identities". plyr.fm operates at the protocol layer, not
the application layer.
- "multiple Bluesky identities" → "multiple ATProto identities"
- "link multiple Bluesky accounts" → "link multiple identities"
- added terminology guidance to status maintenance prompt
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
the previous prompt was ambiguous about what "archiving" meant, leading
to PR #719 condensing cross-references instead of actually moving
content to archive files.
changes:
- explicit line count targets (ideal ~200, acceptable 300-450, max 500)
- clear rule: archive content from PREVIOUS months, keep current month
- step-by-step instructions for CUT/PASTE workflow
- emphasize "archiving = moving content" not "summarizing in place"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The album endpoint was showing ~205ms average but HTTP Request spans
to PDS were showing 0ms duration - an instrumentation gap.
This adds a surgical `logfire.span()` wrapper around `get_record_public()`
to capture actual PDS HTTP call duration without flooding spans like
global httpx instrumentation would.
Span attributes:
- collection: the ATProto collection being fetched
- rkey: the record key
- pds_host: the PDS hostname
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- 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
each db_session() creates a new Neon connection (~77ms overhead).
the multi-account endpoints were creating 3 separate connections:
1. require_auth -> get_session()
2. get_session_group()
3. switch_active_account() or artist lookup
now get_session_group() and switch_active_account() accept optional
db parameter to reuse an existing connection. endpoints pass their
injected db through, reducing connection overhead by ~154ms.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
invalidateAll() only re-runs SvelteKit load functions, but the portal
page loads data client-side in onMount. switching accounts left stale
data on the page. using window.location.reload() ensures fresh data.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* docs: add multi-account experience design spec
research document exploring multi-account UX for users with multiple
ATProto identities. covers OAuth prompt parameter support, session
groups architecture, and phased implementation plan.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* docs: add bluesky implementation study to multi-account spec
studied bluesky's open-source social-app to inform our design:
- session state patterns (accounts array, currentAccount reference)
- UX patterns (avatars, checkmarks, "logged out" labels)
- logout distinction (active only vs all)
- cross-tab sync approach
also updated prerequisite section with link to SDK fork PR #8.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore: update atproto SDK with prompt parameter support
updates to d4830f4 which adds PromptType and prompt parameter
to start_authorization() for multi-account flows.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* docs: expand bluesky implementation study with detailed patterns
- added persistence layer details (AsyncStorage, schema structure)
- documented full SessionAccount interface with all fields
- listed reducer action types for state transitions
- added token refresh strategy comparison
- documented AccountList UI implementation details
- expanded references with direct links to key source files
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: implement multi-account experience
backend:
- add session groups (group_id, is_active, avatar_url columns)
- add pending_add_accounts table for OAuth flow tracking
- add /auth/add-account/start endpoint with prompt=login
- add /auth/switch-account endpoint to switch active session
- add /auth/logout-all endpoint to clear all linked accounts
- update /auth/me to return linked_accounts list
- update callback to handle add-account flow
frontend:
- add LinkedAccount type and update User type
- update UserMenu with account switcher dropdown
- update ProfileMenu with accounts sub-menu for mobile
- show avatars, handles, and switch/add/logout options
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: add-account flow now prompts for new handle
the add-account endpoint was incorrectly using the current user's handle,
which locked the OAuth flow to the same account. now:
- backend: /auth/add-account/start requires a handle in the request body
- backend: validates that handle differs from current account
- frontend: shows inline handle input before starting OAuth flow
- frontend: same UX in UserMenu (desktop) and ProfileMenu (mobile)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: stop propagation on add account click
the click outside handler was closing the menu before the input could
appear because the button was being removed from DOM, causing
menuRef.contains(event.target) to return false.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: deactivate other sessions when adding new account
when a new account is added to a session group, only the new session
should be marked as active. the frontend filters for !is_active to show
switchable accounts, so both being active meant neither showed up.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: filter other accounts by DID, not is_active flag
the cookie determines which session is active - we don't need a separate
is_active flag. just filter out the current user's DID to show other
accounts in the switch list.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: remove unnecessary is_active tracking
the cookie determines which session is active - tracking is_active
separately was redundant complexity.
- removed deactivate_other_sessions_in_group function
- simplified switch_active_account to just validate and return session_id
- switch-account endpoint now checks DID instead of is_active flag
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: remove is_active from API response and types
is_active was never needed - the cookie determines the active session,
and we filter by DID in the frontend. removed from:
- LinkedAccountResponse model
- LinkedAccount TypeScript interface
- handleSwitchAccount checks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: accounts submenu with avatars and logout-all fix
- backend: look up artist avatars in /auth/me for fresh data
- UserMenu: consolidate accounts into collapsible submenu
- ProfileMenu: show current user avatar
- fix: use window.location.href for logout-all to clear state
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: improve multi-account UX
- use invalidateAll() instead of page reload when switching accounts
(prevents "log in" flash during account switch)
- add logout prompt for multi-account users to stay logged in as
another account instead of fully logging out
- add switch_to param to /logout for atomic logout + switch
- fix add-account validation to check ALL accounts in session group
- fix dropdown width (220px) to prevent horizontal expansion
- use HandleAutocomplete in add-account forms
- fix a11y warning in portal page (label → span)
- remove unused .desktop-nav CSS
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: prevent logout button click from closing menu before prompt shows
when clicking logout with multiple accounts, the DOM update that shows
the logout prompt was removing the logout button from the DOM before
the click-outside handler checked containment, causing the menu to close.
added event.stopPropagation() to handleLogoutClick in both UserMenu and
ProfileMenu to prevent the click from bubbling to the document listener.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: convert logout prompt to centered modal
- Create global LogoutState class using Svelte 5 runes
- Add LogoutModal component rendered at root layout level
- Update UserMenu and ProfileMenu to use global logout state
- Modal escapes header's backdrop-filter containing block
- Fix click-outside race condition with stopPropagation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test: add multi-account session management tests
covers session groups, account switching, removal, and pending add-account flow.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: remove is_active and avatar_url from session model
these columns were adding complexity without value:
- is_active: cookie is the source of truth for active session
- avatar_url: /auth/me already fetches fresh from Artist table
simplifies remove_account_from_group to just return first remaining session.
removes unused update_session_avatar function.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* migrate: drop is_active and avatar_url from user_sessions
these columns were added to dev database in earlier development
but the model was simplified before merge. this migration aligns
the database schema with the final model.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: make column drop migration idempotent
the columns were only added to dev database during development.
staging and prod never had them. this migration now checks if
columns exist before attempting to drop them.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- use underscore separator for readability in large integer
- move cryptography imports to module level
- remove unnecessary comments
- import _refresh_locks directly instead of inside each test
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
TTLCache could expire a lock while a coroutine holds a reference to it,
breaking mutual exclusion. LRUCache evicts by recency instead, so locks
actively in use won't be evicted.
Also adds tests verifying:
- same session_id returns same lock
- different sessions have different locks
- cache is bounded by maxsize
- LRU eviction order
Related to #708
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
use async_sessionmaker (SQLAlchemy 2.0) and cache it alongside the engine
instead of recreating it on every db_session() call. while the overhead
is minimal, this follows the intended pattern and reduces object churn.
changes:
- switch from sessionmaker to async_sessionmaker (proper async API)
- add SESSION_MAKERS cache dict alongside ENGINES
- add get_session_maker() function for cached retrieval
Related to #708
feat: send DM notifications when tracks are copyright flagged
- refactor send_track_notification to use _send_dm_to_did
- refactor send_image_flag_notification to use _send_dm_to_did
- all notification methods now have consistent error handling and logfire spans
- reduces code duplication (~50 lines)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- add NotificationResult dataclass with success, error, error_type fields
- categorize errors: dm_blocked, network, auth, unknown
- add logfire spans to _send_dm_to_did and send_copyright_notification
- log warnings with structured data when notifications fail
- return tuple of results so caller knows exactly what happened
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- add send_copyright_notification() to NotificationService
- DM both the artist (uploader) and admin when a track is flagged
- add notified_at field to CopyrightScan model to track notification status
- artist message explains the flag and invites dispute
- admin message includes match details and track link
related to #702
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
changes:
- remove auto-label emission from _store_scan_result (was creating liability)
- add MODERATION_COPYRIGHT_SCORE_THRESHOLD env var (default: 85)
- remove tests that expected auto-label emission
scanning still runs and stores results internally for future use
when we build the notification + action pipeline.
refs #702
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: render links in artist bio
Adds RichText component that auto-detects and renders:
- bare URLs (https://example.com, www.example.com)
- markdown-style links ([text](url))
Links open in new tab with proper security attributes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: align link hover style with existing patterns
remove opacity transition to match .comment-link styling.
add word-break: break-all for long URLs.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Updates the atproto dependency to include the fix for OAuth scope
validation when using permission sets. The PDS expands `include:`
scopes into `repo?collection=` format, which the SDK now handles.
Also updates docs with correct DNS setup (`_lexicon` prefix, not `_atproto`).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
adds permission set lexicon that bundles OAuth permissions under
a human-readable title. users will see "plyr.fm Music Library" instead
of "fm.plyr.track, fm.plyr.like, fm.plyr.comment..."
lexicon:
- fm.plyr.authFullApp: full access for main web app
config:
- ATPROTO_USE_PERMISSION_SETS=true enables permission sets
- defaults to false (granular scopes) until lexicons are published
docs:
- research doc on how permission sets work
- updated lexicons overview with permission set section
to enable: publish lexicon to com.atproto.lexicon.schema on plyr.fm
authority repo, then set ATPROTO_USE_PERMISSION_SETS=true
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
FastAPI was matching /artists/batch as /{did} with did="batch".
Moving the POST /batch route before the GET /{did} route fixes this.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: show atprotofans supporter count and list on artist pages
- add `getAtprotofansProfile` and `getAtprotofansSupporters` API calls
- display supporter count badge in support button
- add supporters section with avatar grid linking to bsky profiles
- responsive styling for mobile
uses atprotofans public endpoints:
- `com.atprotofans.getProfile` for supporter count
- `com.atprotofans.getSupporters` for list of supporters
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: remove count badge from support button, fix avatar handling
- remove supporter count from support button (unnecessary)
- properly check avatar field before rendering
- clean up unused CSS
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: enrich atprotofans supporters with avatars from Bluesky
Fetch each supporter's profile from Bluesky's public API
(app.bsky.actor.getProfile) to get their avatar URL, following
the same pattern used elsewhere in the app where backend
provides avatar_url for display.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: remove frontend Bluesky API calls for avatar enrichment
Don't add external API surface area from the frontend - use what
atprotofans returns directly. If avatars aren't provided, the
placeholder will show. Backend should handle enrichment if needed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: enrich supporters with avatar_url via backend batch endpoint
- Add POST /artists/batch endpoint to get artist data for multiple DIDs
- Frontend calls atprotofans for supporter DIDs, then enriches via our backend
- Uses same pattern as likers: avatar_url comes from Artist table
- Use SensitiveImage wrapper and initials placeholder (consistent with LikersTooltip)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* style: compact overlapping avatar circles for supporters
GitHub-sponsors style: small circles with negative margin overlap,
"+N" badge for overflow. Bounded height regardless of supporter count.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: link supporter avatars to plyr.fm artist pages
Keep users in the app instead of linking to Bluesky.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- frontend: rename `dev` to `run` (keep `dev` as alias for compat)
- add `r` alias to backend and frontend (matches transcoder/moderation)
- add `just tunnel` command for ngrok
- update docs to reference `just frontend run`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add feed/library toggle to maintain consistent header items
When on library page, shows "feed" button to go home.
When not on library, shows "library" button.
This keeps the same number of nav items regardless of page,
preventing layout shifts from space-between redistribution.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: include liked and playlist pages in collection check
Shows feed button on /library, /liked, and /playlist/* pages.
Shows library button everywhere else.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: show feed/library buttons independently
- Feed button: shows when not on homepage
- Library button: shows when not on /library
- Both can show at the same time, space-between handles it
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: redesign header for cleaner desktop layout
- add UserMenu dropdown (handle + portal/settings/logout)
- simplify desktop nav: search | library | upload | user menu
- move social links and stats to LinksMenu only (mobile)
- remove unused SearchTrigger and SettingsMenu components
- update CLAUDE.md to reflect component changes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* style: increase nav item spacing for better visual rhythm
* style: flatten header structure for even space-between distribution
* fix: restore social links (bluesky, status, tangled) to desktop header
* fix: move social links to left of logo (original position)
* feat: add social links to header left margin
Restores Bluesky, status page, and Tangled links in absolutely
positioned left margin area, outside the main header content flow.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: hide social links on narrow desktop screens
Adds media query to hide margin-left when viewport is under 1000px,
preventing overflow when there's insufficient margin space.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: remove nav spacers for proper redistribution
Nav items now naturally redistribute when library/upload links are
hidden on their respective pages, instead of leaving awkward gaps.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update model from claude-3-5-sonnet-latest to claude-sonnet-4-5-20250929.
Tested and verified working with production deployment.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
adds automated scanning of uploaded images (track covers, album covers)
for policy violations using Claude Sonnet vision capabilities.
## moderation service (rust)
- new `/scan-image` endpoint accepts multipart form with image + image_id
- `claude.rs`: Claude API client with vision support
- `image_scans` table for cost tracking and audit trail
- auto-flags unsafe images to `sensitive_images` table
## backend (python)
- `ModerationClient.scan_image()` method for calling the new endpoint
- integration in `upload_track_image()` and album cover upload
- `image_moderation_enabled` setting (default: true)
moderation is best-effort - failures are logged but don't block uploads.
flagged images are blurred in the UI (existing sensitive_images behavior).
closes #166
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: sync avatar on login and add to ATProto profile record
- add optional avatar field to fm.plyr.actor.profile lexicon
- update profile record builder to accept and include avatar
- refresh avatar from bluesky on login sync
- update postgres if avatar changed
- sync avatar to ATProto profile record
fixes stale avatars in likers tooltip and other displays
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* add one-time avatar backfill script
refreshes avatar_url for all artists from bluesky. run with:
cd backend && uv run python ../scripts/backfill_avatars.py --dry-run
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
adds a "top tracks" section above the existing "latest tracks" feed,
showing the 10 most-liked tracks on the platform. this helps surface
quality content instead of the homepage being dominated by bulk uploads.
backend:
- new `/tracks/top` endpoint returning tracks ordered by like count
- `get_top_track_ids()` aggregation helper
frontend:
- fetches top tracks concurrently with latest tracks on mount
- displays section only when there are liked tracks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- add PlyrClient to check track existence via plyr.fm API
- before LLM analysis, check each flagged track still exists
- auto-resolve with reason "content_deleted" if track returns 404
- add ContentDeleted variant to ResolutionReason enum in Rust
prevents labels from persisting in the ether for deleted content
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- move review to /admin/review/:id (proper admin namespace)
- use admin.css for consistent styling with dashboard
- add "← back to dashboard" navigation link
- add three action types:
- clear: false positive, emit negation label
- defer: acknowledge but take no action (flag stays active)
- confirm: mark as real violation (flag stays active)
- toggle decisions by clicking same button again
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>