commits
* perf: defer ATProto sync queries to background task
- move all sync-related DB queries out of /artists/me request path
- queries for albums, tracks, prefs, and likes now run in background
- reduces response time from ~1.2s to ~300ms (only artist lookup needed)
also fix: add trailing slash to playlist search URL (fixes 307 redirect)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: move ATProto sync from /artists/me to login callback
- create sync_atproto_records() function in records.py
- call sync as fire-and-forget background task after OAuth callback
- remove all sync logic from /artists/me (now just returns artist)
- also sync on scope upgrade flow
- update tests to verify new behavior
this ensures ATProto records sync immediately on login rather than
on profile page access, and removes unnecessary work from /artists/me
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add public liked pages for any user (#498)
likes are stored on users' PDSes as ATProto records, making them public data.
this enables viewing any user's liked tracks without authentication.
backend:
- add GET /users/{handle}/likes endpoint
- add get_optional_session dependency for endpoints that benefit from optional auth
- endpoint returns user info, tracks, and count
frontend:
- add /liked/[handle] route with user header and track list
- add fetchUserLikes API function with UserLikesResponse type
- update LikersTooltip to link to user's liked page instead of profile
tests:
- add test_users.py with 4 tests for the new endpoint
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: consolidate optional auth into get_optional_session dependency
replaces inline session_id extraction pattern with reusable FastAPI dependency:
- listing.py: uses get_optional_session instead of manual cookie/header parsing
- playback.py: uses get_optional_session for get_track and increment_play_count
- removes utilities/auth.py (get_session_id_from_request no longer needed)
- updates test_hidden_tags_filter.py to override dependency instead of patching
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add show_liked_on_profile preference
Adds a new user preference (defaults to false) that will allow users
to display their liked tracks on their artist page.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add show_liked_on_profile setting with artist page integration
- Add show_liked_on_profile to preferences API (get/update)
- Expose preference in artist API response for public profiles
- Add settings toggle in frontend privacy & display section
- Update artist page to conditionally fetch and display liked tracks
- Update Preferences interface and layout to include new field
When enabled, a user's liked tracks are displayed on their artist page
below albums, allowing others to discover music they enjoy.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add fm.plyr.list lexicon for playlists and albums
introduces ATProto record type for ordered track collections:
- lexicons/list.json: defines fm.plyr.list with strongRef track references
- purpose field distinguishes "album", "playlist", "collection"
- items array ordering determines display order (reorder via putRecord)
- each item uses strongRef (uri + cid) for content-addressability
backend infrastructure:
- create_list_record/update_list_record in records.py
- list_collection added to OAuth scopes
- exported from _internal.atproto module
design notes:
- strongRef ensures list items point to specific track versions
- when tracks are deleted, list records should be updated to remove refs
- albums can be formalized as list records with purpose="album"
- no records created yet - this is infrastructure for future integration
relates to #221 (ATProto records for albums) and #146 (content-addressable storage)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: generalize fm.plyr.list to support any record type
- renamed listItem.track to listItem.subject for generic references
- added more purpose knownValues: discography, favorites
- updated descriptions to clarify any ATProto record can be referenced
- subject.uri indicates record type (e.g., fm.plyr.track, fm.plyr.list)
enables:
- lists of tracks (albums, playlists)
- lists of albums (discographies)
- lists of lists (playlist collections)
- lists of artists (following, featured)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: simplify fm.plyr.list lexicon to minimal fields
stripped to essentials per lexicon style guide:
- required: items, createdAt
- optional: name (display name), listType (semantic category)
- removed: purpose, description, imageUrl, addedAt
listType knownValues: album, playlist, liked
(extensible - any string valid per ATProto knownValues spec)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add optional updatedAt field to fm.plyr.list
- lexicon: optional updatedAt datetime field
- update_list_record auto-sets updatedAt to now
- create_list_record omits updatedAt (new records)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add feat/playlists branch status to STATUS.md
documents current state of list infrastructure:
- what's done (lexicon, backend functions, oauth scopes)
- what's NOT done (no UI, no records created, no migrations)
- design decisions and next steps
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add fm.plyr.actor.profile lexicon and upsert function
- create profile.json lexicon with bio, createdAt, updatedAt fields
- add profile_collection to AtprotoSettings config
- add profile collection to OAuth scopes
- implement build_profile_record and upsert_profile_record
- uses putRecord with rkey="self" for upsert semantics
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: wire up profile record upsert to artist bio endpoints
- call upsert_profile_record when bio is set on create/update
- handle ATProto failures gracefully (log but don't fail request)
- add tests for profile record integration
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: sync profile record on login with proper background task handling
- add background sync in GET /artists/me for existing users with bios
- skip write if ATProto record already exists with same bio (no-op)
- use proper task lifecycle management to prevent GC before completion
- return None from upsert_profile_record when skipped
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: simplify profile sync to silent fire-and-forget
remove over-engineered response field for toast notification.
profile record sync happens silently in background on GET /artists/me.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: remove unused check_profile_record_sync_needed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: always sync profile record, not just when bio exists
profile record should be created on login regardless of whether
user has a bio set. bio is optional in the lexicon.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: sync albums and liked tracks as ATProto list records on login
Backend:
- Add upsert_album_list_record() and upsert_liked_list_record() functions
- Wire up fire-and-forget sync on GET /artists/me for all artist albums
- Wire up fire-and-forget sync for user's liked tracks list
- Persist ATProto URIs/CIDs back to database after sync
- Migration: add liked_list_uri and liked_list_cid to user_preferences
Frontend:
- Artist page: replace inline liked tracks with link card to /liked/{handle}
- Add "collections" section header to distinguish from albums
- Liked page: handle link now goes to artist page, not Bluesky
Design decisions:
- Liked list references track records directly (not like records) for simplicity
- Array order = display order (ATProto-native approach)
- Albums ordered by track created_at asc, likes by created_at desc
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add library hub page and update navigation
- create /library route as hub for personal collections
- show liked tracks card with count
- add placeholder for future playlists
- change nav from "liked" → "library" (heart icon goes to /library)
- keep /liked route for direct access
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update feat/playlists branch status in STATUS.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: scope upgrade OAuth flow for teal.fm integration (#503)
* feat: add scope upgrade OAuth flow for teal.fm integration
- Add /auth/scope-upgrade/start endpoint that initiates OAuth flow with
expanded scopes (mirrors developer token pattern)
- Replace passive "please re-login" message with immediate OAuth redirect
when user enables teal.fm scrobbling
- Fix preferences bug where toggling settings reset theme to dark mode
(theme is client-side only, preserved from localStorage on fetch)
- Add PendingScopeUpgrade model to track in-flight scope upgrades
- Handle scope_upgraded callback to replace old session with new one
- Add tests for scope upgrade flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update STATUS.md with scope upgrade OAuth flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: resolve test failures from connection pool exhaustion
- add DATABASE_POOL_SIZE=2, DATABASE_MAX_OVERFLOW=0 to pytest env vars
- dispose ENGINES cache after each test in conftest to prevent connection accumulation
- fix mock_refresh_session functions to accept `self` parameter (method signature)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add full-page overlay for developer token display
when users return from OAuth after creating a developer token,
show a prominent overlay so they don't miss it. the token won't
be shown again after dismissing, so this ensures visibility.
- full-page modal with blur backdrop
- copy button with success feedback
- warning text emphasizing save-now urgency
- link to python SDK docs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* docs: update STATUS.md with completed scope upgrade work
- mark feat/scope-invalidation as merged to feat/playlists
- document developer token overlay feature
- document test fixes for connection pool exhaustion
- note all 281 tests passing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: remove confusing album migration note from STATUS.md
albums sync as list records on login - no migration needed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: implement playlist CRUD with ATProto integration
- add playlist endpoints to lists.py (create, list, get, add/remove tracks, delete)
- add Playlist model for database caching of ATProto list records
- add playlist types to frontend (Playlist, PlaylistWithTracks, PlaylistTrack)
- update library page with playlist list and create modal
- fix font inheritance on create button
- filter search results to exclude tracks already in playlist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use API_URL for playlist endpoints and fix button font inheritance
- fix all fetch calls to use API_URL instead of /api/ relative paths
- add font-family: inherit to all modal buttons
- library page: create playlist modal buttons
- playlist page: add-btn, empty-add-btn, cancel/confirm buttons
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add share button and link previews for playlists
- add font-family: inherit to search input in playlist modal
- add font-family: inherit to create playlist input
- add ShareButton component to playlist page (visible to all users)
- add public /lists/playlists/{id}/meta endpoint (no auth required)
- add +page.server.ts to fetch playlist meta for SSR
- add OG meta tags for link previews (og:type, og:title, og:description, twitter:card)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: playlist enhancements and UX improvements
- add "add to playlist" menu to track items (AddToMenu component)
- include playlists in global search results
- filter current playlist from add-to-playlist picker on playlist detail page
- add "create new playlist" link in playlist picker menus
- show playlist artwork in library page list
- fix portal empty playlist state link to open create modal
- update edit button tooltip to "edit playlist metadata"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: portal album/playlist consistency
- use consistent edit icon (document+pencil) for album edit button
- match playlist grid sizing to album grid (200px min, 1.5rem gap)
- match playlist card padding and font sizes to album cards
- update placeholder icon size from 32 to 48
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: standardize liked tracks page buttons to match playlist/album style
* fix: populate is_liked on playlist tracks and resolve svelte-check warnings
* fix: add toast notifications for mutations (playlist CRUD, profile updates)
* feat: graceful degradation for unavailable tracks in playlists
when a track in someone's PDS list no longer exists in the database
(e.g., the track owner deleted it), we now show it grayed out with
"track unavailable" instead of silently hiding it.
- add UnavailableTrack schema to backend
- return unavailable_tracks array from get_playlist endpoint
- render unavailable tracks with muted styling in frontend
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* jpeg fix and a couple other tings
---------
Co-authored-by: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: consolidate user preferences into dedicated settings page
- create /settings route with all preferences consolidated:
- appearance (theme, accent color)
- playback (auto-advance)
- privacy & display (sensitive artwork, timed comments)
- integrations (teal.fm scrobbling)
- developer (API tokens with OAuth flow)
- account (delete with confirmation)
- slim down portal to focus on content management:
- profile settings, tracks, albums, export
- remove preference toggles (moved to /settings)
- remove dev tokens section (moved to /settings)
- remove account deletion (moved to /settings)
- add "all settings →" link to both SettingsMenu and ProfileMenu
- update SensitiveImage tooltip from "enable in portal" to "enable in settings"
- add TokenInfo type for developer tokens
- clean up ~500 lines of unused CSS from portal
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: add font-family inherit to buttons, rename profile → portal in mobile menu
- add font-family: inherit to all buttons in settings page:
revoke-btn, copy-btn, dismiss-btn, create-token-btn,
delete-account-btn, cancel-btn, confirm-delete-btn
- rename mobile menu item from "profile" to "portal" to match route
- update icon to grid layout to better represent portal concept
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: move delete account from settings to portal
Delete account belongs in portal's "your data" section because:
- It's a destructive action on your data, not a preference
- It has an option about AT Protocol records (your data)
- Export is already in portal - delete is the inverse operation
Settings = preferences (theme, colors, toggles, API access)
Portal = your content and data (profile, tracks, albums, export, delete)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add /deploy claude command
automates production deployment with preflight checks:
- verifies clean working tree, on main, up to date
- analyzes changes to determine frontend vs backend
- runs appropriate just target
- pushes to tangled remote
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add bluesky thread link to bufo section in STATUS.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
cache key was just the query, so pattern config changes didn't
invalidate cached results. now includes exclude/include patterns
so config changes take effect immediately.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
adds BUFO_EXCLUDE_PATTERNS and BUFO_INCLUDE_PATTERNS env vars to control
which bufo images appear in the easter egg animation.
- exclude: regex patterns to filter out (default: ^bigbufo_)
- include: allowlist that overrides exclude (default: bigbufo_0_0, bigbufo_2_1)
adds CommaSeparatedStringSet type for parsing comma-delimited env vars.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- sensitive image coverage: complete coverage across all image displays
including media session, embeds, search results, album pages
- mobile artwork upload: iOS HEIC handling via content_type detection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
on iOS, selecting a HEIC photo from the Photos library often results
in a file with a .heic filename but jpeg content (iOS converts on the
fly). the backend was using only the filename extension to validate
image format, causing these files to be silently rejected.
changes:
- backend: add from_content_type() method to ImageFormat
- backend: validate_and_extract() now prefers content_type over extension
- backend: pass image content_type through upload pipeline
- frontend: use accept="image/*" for broader iOS compatibility
- tests: add coverage for iOS HEIC case
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: respect sensitive artwork preference in media session
the media session API (CarPlay, lock screen, control center) was
showing unblurred artwork for tracks flagged as sensitive, even when
the user had `show_sensitive_artwork` disabled.
now checks each artwork candidate against the moderation list and
the user's preference before including it in media session metadata.
if all artwork is sensitive and the user prefers not to see it, the
media session will display no artwork.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: apply SensitiveImage wrapper to remaining image displays
extends sensitive content handling to:
- embed page (track artwork)
- album detail page (artwork + og:image meta tags)
- artist page album grid
- track page comment avatars
- search modal results
- handle search/autocomplete avatars
uses compact mode for small avatars (blur only, no tooltip).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Fix local dev section: uses Neon dev database, not localhost
- Correct Fly.io app name from plyr-api to relay-api throughout
- Add "current capabilities" section documenting that migration
isolation (via release_command) and multi-environment pipeline
(dev/staging/prod) are already implemented
- Update database diagram to show all four databases (dev, staging,
prod on Neon + local test)
- Remove misleading "future considerations" for features that exist
Co-authored-by: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the previous fix only elevated the .likes element, but z-index on a child
doesn't help when sibling track-containers are in the same stacking context.
now:
- .track-container.likers-tooltip-open gets z-index: 60 (above header at 50)
- removes the ineffective .likes.tooltip-open z-index
- the entire track elevates above siblings, ensuring the tooltip renders on top
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
z-index fix:
- remove static z-index from .likes element
- only apply z-index: 10 when tooltip is open (.likes.tooltip-open)
- this ensures the active tooltip's parent elevates above sibling tracks
without all tracks competing at the same z-index level
flip detection:
- detect when likes element is <300px from viewport top
- flip tooltip to appear below instead of above
- prevents tooltip from colliding with sticky header
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* docs: add sensitive image moderation to STATUS.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add sensitive content moderation documentation
documents the sensitive image moderation system:
- database schema (sensitive_images table)
- frontend architecture (SSR + client-side)
- SensitiveImage component usage
- matching logic for R2 and external URLs
- API endpoint
- user experience flow
- current limitations (manual flagging only)
- future improvements needed (perceptual hashing, AI detection)
- moderation workflow with SQL examples
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: likers tooltip z-index and compact mode for sensitive avatars
- add z-index to .likes element so tooltip renders above header border
- add compact prop to SensitiveImage for avatars in lists (blur only, no tooltip, preserves layout)
- use compact mode in LikersTooltip to prevent layout breakage on blurred avatars
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Wrap liker avatars in SensitiveImage component
- Add max-height (240px) and scrolling to likers list
- Fix hover interaction: delay close to allow entering tooltip
- Add Escape key to close tooltip
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: use data prop instead of parent() for sensitive images SSR
The +layout.ts file was incorrectly using parent() to access data from
+layout.server.ts. In SvelteKit, data from a server load function in the
same route comes via the data parameter, not parent(). parent() is only
for accessing data from parent routes.
This fixes SSR meta tag generation for sensitive images.
* fix: handle nullable data parameter in layout load function
TypeScript was complaining that data could be null. Use optional
chaining and extract sensitiveImages to a variable to avoid repetition.
Link previews weren't filtering sensitive images because the moderation
data was only fetched client-side. Now we fetch it in +layout.server.ts
and pass it through to pages for SSR-safe meta tag filtering.
- Add +layout.server.ts to fetch sensitive images
- Export checkImageSensitive() utility for shared use
- Update track and artist pages to use SSR-safe checks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Was blocking all Bluesky avatars. Keep only explicit flagging.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Fix regex to match R2 URLs (pub-*.r2.dev/{id}.{ext})
- Blur non-R2 images by default as they could be injected via ATProto records
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: blur explicit images with user opt-in to show
- add explicit_images table to track flagged image URLs/IDs
- add show_explicit_artwork user preference (default: hidden)
- add /moderation/explicit-images endpoint to fetch flagged images
- add ExplicitImage component that blurs flagged images
- hide explicit images from og:image/twitter meta tags
- add portal toggle for explicit artwork preference
images can be flagged by image_id (R2) or full URL (external avatars).
frontend fetches the list once and checks all rendered images.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: use 'sensitive' instead of 'explicit' in tooltip
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: rename explicit to sensitive throughout codebase
- Rename table: explicit_images → sensitive_images
- Rename model: ExplicitImage → SensitiveImage
- Rename preference: show_explicit_artwork → show_sensitive_artwork
- Rename component: ExplicitImage.svelte → SensitiveImage.svelte
- Update all references, endpoints, and UI text
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
added note about intermittent audio playback hangs in iOS standalone
(PWA) mode. PR #466 added NetworkOnly caching for audio routes, but
iOS Safari is slow to update service workers. workaround is to delete
and re-add home screen bookmark. flagged for further investigation if
issue persists.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Space: play/pause
- Left/Right arrows: seek ±10 seconds
- J/L: previous/next track
- M: mute/unmute
All shortcuts respect input focus and are disabled when search modal is open.
Also fixes AGENTS.md to reflect that STATUS.md is now tracked.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
terminal-based vanity metrics dashboard using plotext for visualization.
features:
- zone stats: requests, pageviews, unique visitors, bandwidth, cache ratio
- daily requests bar chart (cyan)
- pageviews vs uniques line chart (green/magenta)
- CLI options: --days/-d for time window, --no-cache to force refresh
- automatic daily caching (~/.cache/plyr-analytics/)
- pydantic-settings for .env integration (CF_API_TOKEN, CF_ZONE_ID)
usage:
uv run scripts/cf_analytics.py # last 7 days
uv run scripts/cf_analytics.py -d 30 # last 30 days
uv run scripts/cf_analytics.py --no-cache
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the $effect that calls incrementPlayCount() runs on every currentTime
update (~4x/second). when threshold is first crossed, svelte's batched
reactive updates meant the guard (playCountedForTrack === currentTrack.id)
could miss rapid-fire calls, causing 2 scrobbles ~50-125ms apart.
fix: add synchronous (non-reactive) guard that blocks immediately,
before the async fetch fires.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add mutagen dependency for audio metadata extraction
- create backend/utilities/audio.py with extract_duration()
- extract duration during upload, store in track.extra["duration"]
- add backfill script for tracks uploaded before this feature
duration is now correctly passed to teal.fm scrobble records.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add Dec 4 section for PR #467 (teal.fm scrobbling)
- document lexicons, configuration, and code quality improvements
- update now-playing section to reflect native scrobbling
- add teal.fm to "what's working" list
- update last modified date
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* checkpoint
* checkpoint
* feat: teal.fm scrobbling integration
adds teal.fm scrobbling support - when users enable the toggle in settings
and re-authenticate, their plays are recorded to their PDS using teal's
ATProto lexicons.
changes:
- add TealSettings config class for configurable namespaces (TEAL_PLAY_COLLECTION,
TEAL_STATUS_COLLECTION, TEAL_ENABLED env vars)
- fix play count fetch missing credentials: 'include' (root cause of scrobbles
not triggering)
- add preferences.fetch() after login to ensure teal toggle state is current
- add unit tests for env var overrides (proves we can adapt when teal graduates
from alpha namespace)
the teal namespace defaults to fm.teal.alpha.* but can be changed via env vars
when teal.fm updates their lexicons.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: code quality improvements and bug fixes
- walrus operators throughout auth and playback code
- extract bearer token parsing into utilities/auth.py
- move DeveloperTokenInfo to top of file with field_validator for truncation
- fix now-playing firing every 1s (update lastReportedState in scheduled reports)
- use settings.frontend.url/domain for origin URLs (environment-aware)
- move teal.fm scrobbling setting from gear menu to portal "Your Data" section
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
audio streaming was intermittently hanging in iOS standalone (PWA) mode.
the service worker was intercepting /audio/{file_id} requests with
NetworkFirst caching, which caused issues with:
- 307 redirects to R2 CDN getting cached/stale
- range request headers not being handled properly on iOS Safari
switched to NetworkOnly for audio routes so the SW passes through
without interference. we weren't actually caching audio files anyway
(just the redirect response), so there's no functional loss.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
The logout button was rendered outside the isAuthenticated check,
causing both login and logout buttons to appear on desktop when
logged out.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
The search modal was using `visibility: hidden` to hide the backdrop,
which prevented the input from receiving focus. This caused:
- No text cursor on desktop when pressing Cmd+K
- No keyboard popup on mobile when tapping search icon
Changed to use only `opacity: 0` (already present) for hiding.
Elements with `opacity: 0` remain focusable, allowing the synchronous
focus() call in search.open() to work correctly - critical for mobile
keyboards which only open when focus is in the same call stack as the
user gesture.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Removes debug logging that was left in from initial development in PR #233.
Keeps console.error for actual error handling.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
uv run by default syncs all dependencies including dev deps, which
caused the release command to hang downloading ruff, jedi, etc.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the now-playing endpoints were hitting the global 100/minute rate limit
when multiple users/tabs were active simultaneously. since these endpoints
are already throttled client-side (10-second intervals, 1-second debounce,
5-second progress buckets), server-side rate limiting is unnecessary.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the documented commands didn't match actual justfile recipes:
- `just frontend install` -> doesn't exist
- `just run-backend` -> `just backend run`
- `just lint` -> `just backend lint` / `just frontend check`
- `just migrate` -> `just backend migrate`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
when a track doesn't have its own artwork but belongs to an album
with artwork, use the album artwork instead of the placeholder icon.
applies to:
- player artwork thumbnail (TrackInfo.svelte)
- media session metadata for system controls (Player.svelte)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
iOS/mobile browsers only open keyboard when focus() is called directly
in a user gesture handler. Previously, focus happened in a Svelte $effect
after state change, which broke the gesture chain.
Fix: Always render SearchModal (hidden via CSS), register input ref with
search state, and focus directly in search.open() before state change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: move search to top-level mobile header
- Add search icon button to mobile-center nav (left of feed/liked icons)
- Remove search from ProfileMenu dropdown
- Space icons evenly across mobile header
- Cleaner mobile UX with one less tap to search
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: hide keyboard hints on mobile, smaller placeholder text
- Hide Cmd+K shortcut badge on mobile
- Hide keyboard navigation hints (arrows, esc) on mobile
- Shrink placeholder text on mobile to prevent overflow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: collapse excess tags with +N button and fix mobile menu overlap
- limits visible tags to 2 by default (never wraps to second line)
- shows "+N" button when more tags exist
- clicking "+N" expands to show all tags (allows wrapping when expanded)
- uses flex-wrap: nowrap + overflow: hidden to guarantee single-line constraint
- collapses back when track changes (component recycle)
- fix LinksMenu and ProfileMenu mobile positioning to avoid player overlap
- menus now position from bottom with player height offset on mobile
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: keep mobile menus centered but shift up for player
- menus stay full-size and centered
- shift center point up by half player height when player is open
- cap max-height to avoid overlap with player
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add search trigger to header and mobile menu
desktop:
- SearchTrigger component shows magnifying glass + keyboard shortcut hint
- detects platform for correct shortcut (⌘K on Mac, Ctrl+K on Windows/Linux)
- subtle styling with accent highlight on hover
mobile:
- search added as first item in ProfileMenu (three-dot menu)
- styled with accent tint background to stand out
- opens search modal and closes menu
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: hide portal link on portal page, move search to far right
- portal link (@handle) now hidden when already on /portal
- search trigger moved after logout button (far right)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: reorder desktop header - search in nav, logout on far right
- search trigger now in nav: @handle → search → settings
- logout button moved to far right (mirroring stats on left)
- hide portal link when already on /portal page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: swap search and portal link order in nav
nav order now: feed → liked → search → @handle → settings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* more stuff
* yuhhhhhhhh
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: collapse excess tags with +N button and fix mobile menu overlap
- limits visible tags to 2 by default (never wraps to second line)
- shows "+N" button when more tags exist
- clicking "+N" expands to show all tags (allows wrapping when expanded)
- uses flex-wrap: nowrap + overflow: hidden to guarantee single-line constraint
- collapses back when track changes (component recycle)
- fix LinksMenu and ProfileMenu mobile positioning to avoid player overlap
- menus now position from bottom with player height offset on mobile
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: keep mobile menus centered but shift up for player
- menus stay full-size and centered
- shift center point up by half player height when player is open
- cap max-height to avoid overlap with player
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
trigram similarity alone misses cases where a short query
(e.g. "real") is a substring of a word in a long title.
added OR condition with ILIKE to catch exact substring matches
while preserving fuzzy matching behavior for typos.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
replaced text characters (♪, ◉, ◫, #) with actual SVG icons
matching those used elsewhere in the app:
- track: music note icon (same as player placeholder)
- artist: person icon (same as TrackItem/TrackInfo)
- album: record icon (same as TrackItem/TrackInfo)
- tag: label icon (same as tag detail page)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- replaced hardcoded dark rgba colors with CSS variables
- added theme-aware scrollbar styling (scrollbar-color, webkit pseudo-elements)
- modal now respects light/dark/system theme setting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
changed conditional from `commentsEnabled !== false` to
`commentsEnabled === true` so the section only renders
after we explicitly know comments are enabled.
previously: commentsEnabled started as null, which passed
the !== false check, causing a brief flash before the API
response set it to false.
note: no regression test added - frontend lacks test infrastructure.
the fix is a one-line conditional change with clear before/after behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* docs: add unified search documentation
- update keyboard-shortcuts.md with Cmd/Ctrl+K search shortcut
- create comprehensive search.md covering frontend state, backend API,
database indexes (pg_trgm), fuzzy matching, and scaling considerations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add unified search to STATUS.md
- add recent work section for unified search (PR #447)
- move issue #440 from new features to working features
- add search docs to documentation links
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: unified search with Cmd/Ctrl+K shortcut
- add pg_trgm extension and GIN indexes for fuzzy search
- implement /search endpoint with trigram similarity scoring
- search across tracks, artists, albums, and tags
- create SearchModal component with keyboard navigation
- Cmd+K (Mac) / Ctrl+K (Windows/Linux) opens search
- arrow keys navigate, Enter selects, Esc closes
- results grouped by type with relevance scoring
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: improve search modal UX and styling
- use "Cmd+K" text instead of ⌘ symbol for clarity
- add client-side query length validation (max 100 chars)
- show clear error message when query exceeds limit
- apply glassmorphism styling to modal:
- frosted glass background with blur
- subtle border highlights
- refined hover/selected states
- consistent translucent elements
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: show artwork/avatars in search results
- display track artwork, artist avatars, album covers when available
- lazy loading with graceful fallback to icon on error
- image hidden on load failure, icon shown instead
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
STATUS.md:
- add theme system (PR #441): CSS custom properties, dark/light/system toggle
- add mobile UX improvements (PR #443): ProfileMenu, /upload page, portal overhaul
- add player scroll timing fix (PR #445): faster animations, "view track" link
- add CI optimization (PR #444): path-based hook skipping
- add issue #440 to new features list (unified search)
- update last modified date
docs/frontend/toast-notifications.md:
- document action links feature (was listed as "not implemented")
docs/frontend/state-management.md:
- update preferences to include theme and hidden tags
- mention ProfileMenu component for mobile
- fix upload flow to reference /upload page instead of portal
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Player scroll:
- remove the 1.5s hold at -100% (off-screen) position
- text now immediately resets after scrolling off
- reduce title animation from 10s to 8s
- reduce artist/album animation from 15s to 10s
Upload toast:
- fix duplicate "track uploaded successfully" toast
(was showing once on XHR complete, again on SSE complete)
- add "view track" link to success toast with track detail page
- toast action links now stay in same tab for internal URLs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
use dorny/paths-filter to detect what changed and only install
deps that are actually needed:
- frontend changes: install bun + frontend deps
- backend changes: install uv + backend deps
- other changes: no extra deps needed
this avoids installing all deps for every PR, reducing CI time
for frontend-only or backend-only changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Add ProfileMenu component for mobile (collapses profile/upload/settings/logout)
- Extract upload form to dedicated /upload page
- Standardize section headers across home and liked pages
- Comprehensive mobile styling for portal page:
- Tighten profile settings form
- Fix track edit/delete icon alignment with proper SVG icons
- Clean up data section typography
- Add track detail link under artwork
- Fix create token button alignment
- ProfileMenu hides contextual links (profile hidden on /portal, upload hidden on /upload)
- Upload card design in portal links to /upload page
- Fix layout inconsistencies between home and liked pages (padding, font-weight, mobile styles)
- Add globe icon in mobile header to navigate home when not on home page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- onboard: focus on STATUS.md as source of truth, remove verbose instructions
- status-update: align with actual STATUS.md structure, mention auto-archiving
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Replace hardcoded colors across 35 files with CSS custom properties
- Add theme switcher (dark/light/system) in settings menu
- Define semantic color tokens: bg-*, border-*, text-*, accent-*
- Light theme adapts all UI elements including tracks, header, tags
- Remove zen mode feature (was only hiding a few elements)
- Use color-mix() for derived colors to maintain consistency
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
when a track has the 'bufo' tag, semantically-matched toad GIFs
float across the track detail page. uses the track title as a
semantic search query against the find-bufo API.
- BufoEasterEgg component fetches toads based on track title
- results cached in localStorage for 1 week to reduce API calls
- TagEffects wrapper provides extensibility for future tag-based plugins
- animated toads drift across viewport with wobble effects
- respects prefers-reduced-motion
- fails gracefully if API is unavailable
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- track detail endpoint now fetches and returns tags
- album detail endpoint now fetches tags for all tracks
- track detail page displays tags with links to tag pages
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* chore: add ai tag to status maintenance uploads
uses plyrfm >= v0.0.1-alpha.10 which supports -t/--tag option
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update STATUS.md with tag filtering and SDK work
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* perf: defer ATProto sync queries to background task
- move all sync-related DB queries out of /artists/me request path
- queries for albums, tracks, prefs, and likes now run in background
- reduces response time from ~1.2s to ~300ms (only artist lookup needed)
also fix: add trailing slash to playlist search URL (fixes 307 redirect)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: move ATProto sync from /artists/me to login callback
- create sync_atproto_records() function in records.py
- call sync as fire-and-forget background task after OAuth callback
- remove all sync logic from /artists/me (now just returns artist)
- also sync on scope upgrade flow
- update tests to verify new behavior
this ensures ATProto records sync immediately on login rather than
on profile page access, and removes unnecessary work from /artists/me
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add public liked pages for any user (#498)
likes are stored on users' PDSes as ATProto records, making them public data.
this enables viewing any user's liked tracks without authentication.
backend:
- add GET /users/{handle}/likes endpoint
- add get_optional_session dependency for endpoints that benefit from optional auth
- endpoint returns user info, tracks, and count
frontend:
- add /liked/[handle] route with user header and track list
- add fetchUserLikes API function with UserLikesResponse type
- update LikersTooltip to link to user's liked page instead of profile
tests:
- add test_users.py with 4 tests for the new endpoint
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: consolidate optional auth into get_optional_session dependency
replaces inline session_id extraction pattern with reusable FastAPI dependency:
- listing.py: uses get_optional_session instead of manual cookie/header parsing
- playback.py: uses get_optional_session for get_track and increment_play_count
- removes utilities/auth.py (get_session_id_from_request no longer needed)
- updates test_hidden_tags_filter.py to override dependency instead of patching
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add show_liked_on_profile preference
Adds a new user preference (defaults to false) that will allow users
to display their liked tracks on their artist page.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add show_liked_on_profile setting with artist page integration
- Add show_liked_on_profile to preferences API (get/update)
- Expose preference in artist API response for public profiles
- Add settings toggle in frontend privacy & display section
- Update artist page to conditionally fetch and display liked tracks
- Update Preferences interface and layout to include new field
When enabled, a user's liked tracks are displayed on their artist page
below albums, allowing others to discover music they enjoy.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add fm.plyr.list lexicon for playlists and albums
introduces ATProto record type for ordered track collections:
- lexicons/list.json: defines fm.plyr.list with strongRef track references
- purpose field distinguishes "album", "playlist", "collection"
- items array ordering determines display order (reorder via putRecord)
- each item uses strongRef (uri + cid) for content-addressability
backend infrastructure:
- create_list_record/update_list_record in records.py
- list_collection added to OAuth scopes
- exported from _internal.atproto module
design notes:
- strongRef ensures list items point to specific track versions
- when tracks are deleted, list records should be updated to remove refs
- albums can be formalized as list records with purpose="album"
- no records created yet - this is infrastructure for future integration
relates to #221 (ATProto records for albums) and #146 (content-addressable storage)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: generalize fm.plyr.list to support any record type
- renamed listItem.track to listItem.subject for generic references
- added more purpose knownValues: discography, favorites
- updated descriptions to clarify any ATProto record can be referenced
- subject.uri indicates record type (e.g., fm.plyr.track, fm.plyr.list)
enables:
- lists of tracks (albums, playlists)
- lists of albums (discographies)
- lists of lists (playlist collections)
- lists of artists (following, featured)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: simplify fm.plyr.list lexicon to minimal fields
stripped to essentials per lexicon style guide:
- required: items, createdAt
- optional: name (display name), listType (semantic category)
- removed: purpose, description, imageUrl, addedAt
listType knownValues: album, playlist, liked
(extensible - any string valid per ATProto knownValues spec)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add optional updatedAt field to fm.plyr.list
- lexicon: optional updatedAt datetime field
- update_list_record auto-sets updatedAt to now
- create_list_record omits updatedAt (new records)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add feat/playlists branch status to STATUS.md
documents current state of list infrastructure:
- what's done (lexicon, backend functions, oauth scopes)
- what's NOT done (no UI, no records created, no migrations)
- design decisions and next steps
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add fm.plyr.actor.profile lexicon and upsert function
- create profile.json lexicon with bio, createdAt, updatedAt fields
- add profile_collection to AtprotoSettings config
- add profile collection to OAuth scopes
- implement build_profile_record and upsert_profile_record
- uses putRecord with rkey="self" for upsert semantics
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: wire up profile record upsert to artist bio endpoints
- call upsert_profile_record when bio is set on create/update
- handle ATProto failures gracefully (log but don't fail request)
- add tests for profile record integration
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: sync profile record on login with proper background task handling
- add background sync in GET /artists/me for existing users with bios
- skip write if ATProto record already exists with same bio (no-op)
- use proper task lifecycle management to prevent GC before completion
- return None from upsert_profile_record when skipped
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: simplify profile sync to silent fire-and-forget
remove over-engineered response field for toast notification.
profile record sync happens silently in background on GET /artists/me.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: remove unused check_profile_record_sync_needed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: always sync profile record, not just when bio exists
profile record should be created on login regardless of whether
user has a bio set. bio is optional in the lexicon.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: sync albums and liked tracks as ATProto list records on login
Backend:
- Add upsert_album_list_record() and upsert_liked_list_record() functions
- Wire up fire-and-forget sync on GET /artists/me for all artist albums
- Wire up fire-and-forget sync for user's liked tracks list
- Persist ATProto URIs/CIDs back to database after sync
- Migration: add liked_list_uri and liked_list_cid to user_preferences
Frontend:
- Artist page: replace inline liked tracks with link card to /liked/{handle}
- Add "collections" section header to distinguish from albums
- Liked page: handle link now goes to artist page, not Bluesky
Design decisions:
- Liked list references track records directly (not like records) for simplicity
- Array order = display order (ATProto-native approach)
- Albums ordered by track created_at asc, likes by created_at desc
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add library hub page and update navigation
- create /library route as hub for personal collections
- show liked tracks card with count
- add placeholder for future playlists
- change nav from "liked" → "library" (heart icon goes to /library)
- keep /liked route for direct access
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update feat/playlists branch status in STATUS.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: scope upgrade OAuth flow for teal.fm integration (#503)
* feat: add scope upgrade OAuth flow for teal.fm integration
- Add /auth/scope-upgrade/start endpoint that initiates OAuth flow with
expanded scopes (mirrors developer token pattern)
- Replace passive "please re-login" message with immediate OAuth redirect
when user enables teal.fm scrobbling
- Fix preferences bug where toggling settings reset theme to dark mode
(theme is client-side only, preserved from localStorage on fetch)
- Add PendingScopeUpgrade model to track in-flight scope upgrades
- Handle scope_upgraded callback to replace old session with new one
- Add tests for scope upgrade flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update STATUS.md with scope upgrade OAuth flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: resolve test failures from connection pool exhaustion
- add DATABASE_POOL_SIZE=2, DATABASE_MAX_OVERFLOW=0 to pytest env vars
- dispose ENGINES cache after each test in conftest to prevent connection accumulation
- fix mock_refresh_session functions to accept `self` parameter (method signature)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add full-page overlay for developer token display
when users return from OAuth after creating a developer token,
show a prominent overlay so they don't miss it. the token won't
be shown again after dismissing, so this ensures visibility.
- full-page modal with blur backdrop
- copy button with success feedback
- warning text emphasizing save-now urgency
- link to python SDK docs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* docs: update STATUS.md with completed scope upgrade work
- mark feat/scope-invalidation as merged to feat/playlists
- document developer token overlay feature
- document test fixes for connection pool exhaustion
- note all 281 tests passing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: remove confusing album migration note from STATUS.md
albums sync as list records on login - no migration needed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: implement playlist CRUD with ATProto integration
- add playlist endpoints to lists.py (create, list, get, add/remove tracks, delete)
- add Playlist model for database caching of ATProto list records
- add playlist types to frontend (Playlist, PlaylistWithTracks, PlaylistTrack)
- update library page with playlist list and create modal
- fix font inheritance on create button
- filter search results to exclude tracks already in playlist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use API_URL for playlist endpoints and fix button font inheritance
- fix all fetch calls to use API_URL instead of /api/ relative paths
- add font-family: inherit to all modal buttons
- library page: create playlist modal buttons
- playlist page: add-btn, empty-add-btn, cancel/confirm buttons
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add share button and link previews for playlists
- add font-family: inherit to search input in playlist modal
- add font-family: inherit to create playlist input
- add ShareButton component to playlist page (visible to all users)
- add public /lists/playlists/{id}/meta endpoint (no auth required)
- add +page.server.ts to fetch playlist meta for SSR
- add OG meta tags for link previews (og:type, og:title, og:description, twitter:card)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: playlist enhancements and UX improvements
- add "add to playlist" menu to track items (AddToMenu component)
- include playlists in global search results
- filter current playlist from add-to-playlist picker on playlist detail page
- add "create new playlist" link in playlist picker menus
- show playlist artwork in library page list
- fix portal empty playlist state link to open create modal
- update edit button tooltip to "edit playlist metadata"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: portal album/playlist consistency
- use consistent edit icon (document+pencil) for album edit button
- match playlist grid sizing to album grid (200px min, 1.5rem gap)
- match playlist card padding and font sizes to album cards
- update placeholder icon size from 32 to 48
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: standardize liked tracks page buttons to match playlist/album style
* fix: populate is_liked on playlist tracks and resolve svelte-check warnings
* fix: add toast notifications for mutations (playlist CRUD, profile updates)
* feat: graceful degradation for unavailable tracks in playlists
when a track in someone's PDS list no longer exists in the database
(e.g., the track owner deleted it), we now show it grayed out with
"track unavailable" instead of silently hiding it.
- add UnavailableTrack schema to backend
- return unavailable_tracks array from get_playlist endpoint
- render unavailable tracks with muted styling in frontend
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* jpeg fix and a couple other tings
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: consolidate user preferences into dedicated settings page
- create /settings route with all preferences consolidated:
- appearance (theme, accent color)
- playback (auto-advance)
- privacy & display (sensitive artwork, timed comments)
- integrations (teal.fm scrobbling)
- developer (API tokens with OAuth flow)
- account (delete with confirmation)
- slim down portal to focus on content management:
- profile settings, tracks, albums, export
- remove preference toggles (moved to /settings)
- remove dev tokens section (moved to /settings)
- remove account deletion (moved to /settings)
- add "all settings →" link to both SettingsMenu and ProfileMenu
- update SensitiveImage tooltip from "enable in portal" to "enable in settings"
- add TokenInfo type for developer tokens
- clean up ~500 lines of unused CSS from portal
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: add font-family inherit to buttons, rename profile → portal in mobile menu
- add font-family: inherit to all buttons in settings page:
revoke-btn, copy-btn, dismiss-btn, create-token-btn,
delete-account-btn, cancel-btn, confirm-delete-btn
- rename mobile menu item from "profile" to "portal" to match route
- update icon to grid layout to better represent portal concept
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: move delete account from settings to portal
Delete account belongs in portal's "your data" section because:
- It's a destructive action on your data, not a preference
- It has an option about AT Protocol records (your data)
- Export is already in portal - delete is the inverse operation
Settings = preferences (theme, colors, toggles, API access)
Portal = your content and data (profile, tracks, albums, export, delete)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add /deploy claude command
automates production deployment with preflight checks:
- verifies clean working tree, on main, up to date
- analyzes changes to determine frontend vs backend
- runs appropriate just target
- pushes to tangled remote
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add bluesky thread link to bufo section in STATUS.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
cache key was just the query, so pattern config changes didn't
invalidate cached results. now includes exclude/include patterns
so config changes take effect immediately.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
adds BUFO_EXCLUDE_PATTERNS and BUFO_INCLUDE_PATTERNS env vars to control
which bufo images appear in the easter egg animation.
- exclude: regex patterns to filter out (default: ^bigbufo_)
- include: allowlist that overrides exclude (default: bigbufo_0_0, bigbufo_2_1)
adds CommaSeparatedStringSet type for parsing comma-delimited env vars.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- sensitive image coverage: complete coverage across all image displays
including media session, embeds, search results, album pages
- mobile artwork upload: iOS HEIC handling via content_type detection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
on iOS, selecting a HEIC photo from the Photos library often results
in a file with a .heic filename but jpeg content (iOS converts on the
fly). the backend was using only the filename extension to validate
image format, causing these files to be silently rejected.
changes:
- backend: add from_content_type() method to ImageFormat
- backend: validate_and_extract() now prefers content_type over extension
- backend: pass image content_type through upload pipeline
- frontend: use accept="image/*" for broader iOS compatibility
- tests: add coverage for iOS HEIC case
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: respect sensitive artwork preference in media session
the media session API (CarPlay, lock screen, control center) was
showing unblurred artwork for tracks flagged as sensitive, even when
the user had `show_sensitive_artwork` disabled.
now checks each artwork candidate against the moderation list and
the user's preference before including it in media session metadata.
if all artwork is sensitive and the user prefers not to see it, the
media session will display no artwork.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: apply SensitiveImage wrapper to remaining image displays
extends sensitive content handling to:
- embed page (track artwork)
- album detail page (artwork + og:image meta tags)
- artist page album grid
- track page comment avatars
- search modal results
- handle search/autocomplete avatars
uses compact mode for small avatars (blur only, no tooltip).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Fix local dev section: uses Neon dev database, not localhost
- Correct Fly.io app name from plyr-api to relay-api throughout
- Add "current capabilities" section documenting that migration
isolation (via release_command) and multi-environment pipeline
(dev/staging/prod) are already implemented
- Update database diagram to show all four databases (dev, staging,
prod on Neon + local test)
- Remove misleading "future considerations" for features that exist
Co-authored-by: Claude <noreply@anthropic.com>
the previous fix only elevated the .likes element, but z-index on a child
doesn't help when sibling track-containers are in the same stacking context.
now:
- .track-container.likers-tooltip-open gets z-index: 60 (above header at 50)
- removes the ineffective .likes.tooltip-open z-index
- the entire track elevates above siblings, ensuring the tooltip renders on top
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
z-index fix:
- remove static z-index from .likes element
- only apply z-index: 10 when tooltip is open (.likes.tooltip-open)
- this ensures the active tooltip's parent elevates above sibling tracks
without all tracks competing at the same z-index level
flip detection:
- detect when likes element is <300px from viewport top
- flip tooltip to appear below instead of above
- prevents tooltip from colliding with sticky header
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* docs: add sensitive image moderation to STATUS.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add sensitive content moderation documentation
documents the sensitive image moderation system:
- database schema (sensitive_images table)
- frontend architecture (SSR + client-side)
- SensitiveImage component usage
- matching logic for R2 and external URLs
- API endpoint
- user experience flow
- current limitations (manual flagging only)
- future improvements needed (perceptual hashing, AI detection)
- moderation workflow with SQL examples
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: likers tooltip z-index and compact mode for sensitive avatars
- add z-index to .likes element so tooltip renders above header border
- add compact prop to SensitiveImage for avatars in lists (blur only, no tooltip, preserves layout)
- use compact mode in LikersTooltip to prevent layout breakage on blurred avatars
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Wrap liker avatars in SensitiveImage component
- Add max-height (240px) and scrolling to likers list
- Fix hover interaction: delay close to allow entering tooltip
- Add Escape key to close tooltip
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: use data prop instead of parent() for sensitive images SSR
The +layout.ts file was incorrectly using parent() to access data from
+layout.server.ts. In SvelteKit, data from a server load function in the
same route comes via the data parameter, not parent(). parent() is only
for accessing data from parent routes.
This fixes SSR meta tag generation for sensitive images.
* fix: handle nullable data parameter in layout load function
TypeScript was complaining that data could be null. Use optional
chaining and extract sensitiveImages to a variable to avoid repetition.
Link previews weren't filtering sensitive images because the moderation
data was only fetched client-side. Now we fetch it in +layout.server.ts
and pass it through to pages for SSR-safe meta tag filtering.
- Add +layout.server.ts to fetch sensitive images
- Export checkImageSensitive() utility for shared use
- Update track and artist pages to use SSR-safe checks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Was blocking all Bluesky avatars. Keep only explicit flagging.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: blur explicit images with user opt-in to show
- add explicit_images table to track flagged image URLs/IDs
- add show_explicit_artwork user preference (default: hidden)
- add /moderation/explicit-images endpoint to fetch flagged images
- add ExplicitImage component that blurs flagged images
- hide explicit images from og:image/twitter meta tags
- add portal toggle for explicit artwork preference
images can be flagged by image_id (R2) or full URL (external avatars).
frontend fetches the list once and checks all rendered images.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: use 'sensitive' instead of 'explicit' in tooltip
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: rename explicit to sensitive throughout codebase
- Rename table: explicit_images → sensitive_images
- Rename model: ExplicitImage → SensitiveImage
- Rename preference: show_explicit_artwork → show_sensitive_artwork
- Rename component: ExplicitImage.svelte → SensitiveImage.svelte
- Update all references, endpoints, and UI text
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
added note about intermittent audio playback hangs in iOS standalone
(PWA) mode. PR #466 added NetworkOnly caching for audio routes, but
iOS Safari is slow to update service workers. workaround is to delete
and re-add home screen bookmark. flagged for further investigation if
issue persists.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Space: play/pause
- Left/Right arrows: seek ±10 seconds
- J/L: previous/next track
- M: mute/unmute
All shortcuts respect input focus and are disabled when search modal is open.
Also fixes AGENTS.md to reflect that STATUS.md is now tracked.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
terminal-based vanity metrics dashboard using plotext for visualization.
features:
- zone stats: requests, pageviews, unique visitors, bandwidth, cache ratio
- daily requests bar chart (cyan)
- pageviews vs uniques line chart (green/magenta)
- CLI options: --days/-d for time window, --no-cache to force refresh
- automatic daily caching (~/.cache/plyr-analytics/)
- pydantic-settings for .env integration (CF_API_TOKEN, CF_ZONE_ID)
usage:
uv run scripts/cf_analytics.py # last 7 days
uv run scripts/cf_analytics.py -d 30 # last 30 days
uv run scripts/cf_analytics.py --no-cache
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the $effect that calls incrementPlayCount() runs on every currentTime
update (~4x/second). when threshold is first crossed, svelte's batched
reactive updates meant the guard (playCountedForTrack === currentTrack.id)
could miss rapid-fire calls, causing 2 scrobbles ~50-125ms apart.
fix: add synchronous (non-reactive) guard that blocks immediately,
before the async fetch fires.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add mutagen dependency for audio metadata extraction
- create backend/utilities/audio.py with extract_duration()
- extract duration during upload, store in track.extra["duration"]
- add backfill script for tracks uploaded before this feature
duration is now correctly passed to teal.fm scrobble records.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add Dec 4 section for PR #467 (teal.fm scrobbling)
- document lexicons, configuration, and code quality improvements
- update now-playing section to reflect native scrobbling
- add teal.fm to "what's working" list
- update last modified date
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* checkpoint
* checkpoint
* feat: teal.fm scrobbling integration
adds teal.fm scrobbling support - when users enable the toggle in settings
and re-authenticate, their plays are recorded to their PDS using teal's
ATProto lexicons.
changes:
- add TealSettings config class for configurable namespaces (TEAL_PLAY_COLLECTION,
TEAL_STATUS_COLLECTION, TEAL_ENABLED env vars)
- fix play count fetch missing credentials: 'include' (root cause of scrobbles
not triggering)
- add preferences.fetch() after login to ensure teal toggle state is current
- add unit tests for env var overrides (proves we can adapt when teal graduates
from alpha namespace)
the teal namespace defaults to fm.teal.alpha.* but can be changed via env vars
when teal.fm updates their lexicons.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: code quality improvements and bug fixes
- walrus operators throughout auth and playback code
- extract bearer token parsing into utilities/auth.py
- move DeveloperTokenInfo to top of file with field_validator for truncation
- fix now-playing firing every 1s (update lastReportedState in scheduled reports)
- use settings.frontend.url/domain for origin URLs (environment-aware)
- move teal.fm scrobbling setting from gear menu to portal "Your Data" section
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
audio streaming was intermittently hanging in iOS standalone (PWA) mode.
the service worker was intercepting /audio/{file_id} requests with
NetworkFirst caching, which caused issues with:
- 307 redirects to R2 CDN getting cached/stale
- range request headers not being handled properly on iOS Safari
switched to NetworkOnly for audio routes so the SW passes through
without interference. we weren't actually caching audio files anyway
(just the redirect response), so there's no functional loss.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
The search modal was using `visibility: hidden` to hide the backdrop,
which prevented the input from receiving focus. This caused:
- No text cursor on desktop when pressing Cmd+K
- No keyboard popup on mobile when tapping search icon
Changed to use only `opacity: 0` (already present) for hiding.
Elements with `opacity: 0` remain focusable, allowing the synchronous
focus() call in search.open() to work correctly - critical for mobile
keyboards which only open when focus is in the same call stack as the
user gesture.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the now-playing endpoints were hitting the global 100/minute rate limit
when multiple users/tabs were active simultaneously. since these endpoints
are already throttled client-side (10-second intervals, 1-second debounce,
5-second progress buckets), server-side rate limiting is unnecessary.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the documented commands didn't match actual justfile recipes:
- `just frontend install` -> doesn't exist
- `just run-backend` -> `just backend run`
- `just lint` -> `just backend lint` / `just frontend check`
- `just migrate` -> `just backend migrate`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
when a track doesn't have its own artwork but belongs to an album
with artwork, use the album artwork instead of the placeholder icon.
applies to:
- player artwork thumbnail (TrackInfo.svelte)
- media session metadata for system controls (Player.svelte)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
iOS/mobile browsers only open keyboard when focus() is called directly
in a user gesture handler. Previously, focus happened in a Svelte $effect
after state change, which broke the gesture chain.
Fix: Always render SearchModal (hidden via CSS), register input ref with
search state, and focus directly in search.open() before state change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: move search to top-level mobile header
- Add search icon button to mobile-center nav (left of feed/liked icons)
- Remove search from ProfileMenu dropdown
- Space icons evenly across mobile header
- Cleaner mobile UX with one less tap to search
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: hide keyboard hints on mobile, smaller placeholder text
- Hide Cmd+K shortcut badge on mobile
- Hide keyboard navigation hints (arrows, esc) on mobile
- Shrink placeholder text on mobile to prevent overflow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: collapse excess tags with +N button and fix mobile menu overlap
- limits visible tags to 2 by default (never wraps to second line)
- shows "+N" button when more tags exist
- clicking "+N" expands to show all tags (allows wrapping when expanded)
- uses flex-wrap: nowrap + overflow: hidden to guarantee single-line constraint
- collapses back when track changes (component recycle)
- fix LinksMenu and ProfileMenu mobile positioning to avoid player overlap
- menus now position from bottom with player height offset on mobile
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: keep mobile menus centered but shift up for player
- menus stay full-size and centered
- shift center point up by half player height when player is open
- cap max-height to avoid overlap with player
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add search trigger to header and mobile menu
desktop:
- SearchTrigger component shows magnifying glass + keyboard shortcut hint
- detects platform for correct shortcut (⌘K on Mac, Ctrl+K on Windows/Linux)
- subtle styling with accent highlight on hover
mobile:
- search added as first item in ProfileMenu (three-dot menu)
- styled with accent tint background to stand out
- opens search modal and closes menu
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: hide portal link on portal page, move search to far right
- portal link (@handle) now hidden when already on /portal
- search trigger moved after logout button (far right)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: reorder desktop header - search in nav, logout on far right
- search trigger now in nav: @handle → search → settings
- logout button moved to far right (mirroring stats on left)
- hide portal link when already on /portal page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: swap search and portal link order in nav
nav order now: feed → liked → search → @handle → settings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* more stuff
* yuhhhhhhhh
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: collapse excess tags with +N button and fix mobile menu overlap
- limits visible tags to 2 by default (never wraps to second line)
- shows "+N" button when more tags exist
- clicking "+N" expands to show all tags (allows wrapping when expanded)
- uses flex-wrap: nowrap + overflow: hidden to guarantee single-line constraint
- collapses back when track changes (component recycle)
- fix LinksMenu and ProfileMenu mobile positioning to avoid player overlap
- menus now position from bottom with player height offset on mobile
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: keep mobile menus centered but shift up for player
- menus stay full-size and centered
- shift center point up by half player height when player is open
- cap max-height to avoid overlap with player
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
trigram similarity alone misses cases where a short query
(e.g. "real") is a substring of a word in a long title.
added OR condition with ILIKE to catch exact substring matches
while preserving fuzzy matching behavior for typos.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
replaced text characters (♪, ◉, ◫, #) with actual SVG icons
matching those used elsewhere in the app:
- track: music note icon (same as player placeholder)
- artist: person icon (same as TrackItem/TrackInfo)
- album: record icon (same as TrackItem/TrackInfo)
- tag: label icon (same as tag detail page)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
changed conditional from `commentsEnabled !== false` to
`commentsEnabled === true` so the section only renders
after we explicitly know comments are enabled.
previously: commentsEnabled started as null, which passed
the !== false check, causing a brief flash before the API
response set it to false.
note: no regression test added - frontend lacks test infrastructure.
the fix is a one-line conditional change with clear before/after behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* docs: add unified search documentation
- update keyboard-shortcuts.md with Cmd/Ctrl+K search shortcut
- create comprehensive search.md covering frontend state, backend API,
database indexes (pg_trgm), fuzzy matching, and scaling considerations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add unified search to STATUS.md
- add recent work section for unified search (PR #447)
- move issue #440 from new features to working features
- add search docs to documentation links
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: unified search with Cmd/Ctrl+K shortcut
- add pg_trgm extension and GIN indexes for fuzzy search
- implement /search endpoint with trigram similarity scoring
- search across tracks, artists, albums, and tags
- create SearchModal component with keyboard navigation
- Cmd+K (Mac) / Ctrl+K (Windows/Linux) opens search
- arrow keys navigate, Enter selects, Esc closes
- results grouped by type with relevance scoring
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: improve search modal UX and styling
- use "Cmd+K" text instead of ⌘ symbol for clarity
- add client-side query length validation (max 100 chars)
- show clear error message when query exceeds limit
- apply glassmorphism styling to modal:
- frosted glass background with blur
- subtle border highlights
- refined hover/selected states
- consistent translucent elements
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: show artwork/avatars in search results
- display track artwork, artist avatars, album covers when available
- lazy loading with graceful fallback to icon on error
- image hidden on load failure, icon shown instead
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
STATUS.md:
- add theme system (PR #441): CSS custom properties, dark/light/system toggle
- add mobile UX improvements (PR #443): ProfileMenu, /upload page, portal overhaul
- add player scroll timing fix (PR #445): faster animations, "view track" link
- add CI optimization (PR #444): path-based hook skipping
- add issue #440 to new features list (unified search)
- update last modified date
docs/frontend/toast-notifications.md:
- document action links feature (was listed as "not implemented")
docs/frontend/state-management.md:
- update preferences to include theme and hidden tags
- mention ProfileMenu component for mobile
- fix upload flow to reference /upload page instead of portal
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Player scroll:
- remove the 1.5s hold at -100% (off-screen) position
- text now immediately resets after scrolling off
- reduce title animation from 10s to 8s
- reduce artist/album animation from 15s to 10s
Upload toast:
- fix duplicate "track uploaded successfully" toast
(was showing once on XHR complete, again on SSE complete)
- add "view track" link to success toast with track detail page
- toast action links now stay in same tab for internal URLs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
use dorny/paths-filter to detect what changed and only install
deps that are actually needed:
- frontend changes: install bun + frontend deps
- backend changes: install uv + backend deps
- other changes: no extra deps needed
this avoids installing all deps for every PR, reducing CI time
for frontend-only or backend-only changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Add ProfileMenu component for mobile (collapses profile/upload/settings/logout)
- Extract upload form to dedicated /upload page
- Standardize section headers across home and liked pages
- Comprehensive mobile styling for portal page:
- Tighten profile settings form
- Fix track edit/delete icon alignment with proper SVG icons
- Clean up data section typography
- Add track detail link under artwork
- Fix create token button alignment
- ProfileMenu hides contextual links (profile hidden on /portal, upload hidden on /upload)
- Upload card design in portal links to /upload page
- Fix layout inconsistencies between home and liked pages (padding, font-weight, mobile styles)
- Add globe icon in mobile header to navigate home when not on home page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Replace hardcoded colors across 35 files with CSS custom properties
- Add theme switcher (dark/light/system) in settings menu
- Define semantic color tokens: bg-*, border-*, text-*, accent-*
- Light theme adapts all UI elements including tracks, header, tags
- Remove zen mode feature (was only hiding a few elements)
- Use color-mix() for derived colors to maintain consistency
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
when a track has the 'bufo' tag, semantically-matched toad GIFs
float across the track detail page. uses the track title as a
semantic search query against the find-bufo API.
- BufoEasterEgg component fetches toads based on track title
- results cached in localStorage for 1 week to reduce API calls
- TagEffects wrapper provides extensibility for future tag-based plugins
- animated toads drift across viewport with wobble effects
- respects prefers-reduced-motion
- fails gracefully if API is unavailable
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* chore: add ai tag to status maintenance uploads
uses plyrfm >= v0.0.1-alpha.10 which supports -t/--tag option
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update STATUS.md with tag filtering and SDK work
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>