commits
Update album title in UI immediately instead of blocking on backend
response. Toast appears when backend completes, and UI reverts on error.
This makes the edit feel much snappier since the backend call can take
several seconds (updating all track ATProto records + list record).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
When an album title is updated via PATCH /albums/{album_id}, we now also
update the album's ATProto list record with the new name. Previously only
track records were updated, leaving the list record with the stale name.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: album delete endpoint and track ordering from ATProto list
fixes two user-reported issues:
1. album deletion: adds DELETE /albums/{album_id} endpoint
- by default orphans tracks (sets album_id to null)
- with ?cascade=true deletes all tracks in the album
- cleans up ATProto list record and cover image
2. album track ordering: respects ATProto list record order
- get_album now fetches the ATProto list record to determine track order
- falls back to created_at order if no ATProto record exists
- fixes issue where reordering tracks in the frontend didn't persist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: album edit mode with ATProto sync and portal UX improvements
- add PATCH /albums/{album_id} endpoint for title/description updates
- sync album title changes to all track ATProto records
- add DELETE /albums/{album_id}/tracks/{track_id} to remove tracks from albums
- implement album page edit mode (inline title, cover upload, delete album)
- simplify portal albums section to clickable links (matches playlist UX)
- fix icon buttons to use square shape matching playlist page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- created plyr-stats bucket with public dev-url access
- costs data now lives in dedicated bucket, shared across all envs
- removes dependency on environment-specific audio buckets
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: public costs dashboard with daily export to R2
- add /costs page showing monthly infrastructure costs
- costs.json exported daily via GitHub Action to R2
- backend /config endpoint exposes costs_json_url
- ko-fi support link for community funding
- hardcoded Fly/Neon/Cloudflare costs, dynamic AudD from DB
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: correct audd cost calculation with one-time adjustment
the copyright_scans table was created Nov 24 but first scan recorded
Nov 30, so database counts are incomplete for this billing period.
- hardcode audd values for Nov 24 - Dec 24 from dashboard (6781 requests, $3.91)
- after Dec 24, automatically uses live database counts
- includes cleanup comment to remove hardcoded block after transition
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update STATUS.md - cost dashboard complete
- add public cost dashboard to recent work section
- remove from immediate priorities (done)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* docs: audit and update documentation
- promote connection-pool-exhaustion runbook from sandbox to docs/runbooks/
- update CLAUDE.md files with current module descriptions
- _internal: add background tasks, moderation, jobs
- api: add albums, playlists, exports, moderation, stats
- docs: add runbooks, testing, moderation, lexicons
- refresh docs/README.md with streamlined navigation
- update STATUS.md:
- add Dec 9 docket/concurrent exports section
- update immediate priorities (moderation cleanup, cost dashboard)
- de-emphasize transcoder (moved to backlog)
- add docket to technical architecture
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update README with current state
- add docket, logfire to tech stack
- add moderation/transcoder services section
- update local dev to include dev-services (redis)
- add useful commands section with actual justfile targets
- update features (playlists, scrobbling, timed comments, support links)
- add data ownership section
- update project structure (_internal services)
- consolidate links + documentation into single section
- add mirrors section (github, tangled)
- remove stale atproto fork reference
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
terminal dashboard for monitoring AudD copyright scan API usage
and costs. tracks scans per billing period (24th of month), shows
remaining free requests, estimated costs, and daily breakdown with
plotext charts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
previously tracks were downloaded sequentially, making exports slow
for users with many tracks or large files. now downloads happen
concurrently (up to 4 at a time) while zip creation remains sequential
(zipfile isn't thread-safe).
- refactor process_export to use asyncio.gather + semaphore
- add regression test verifying concurrent download behavior
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: migrate ATProto sync and teal scrobbling to docket
- add sync_atproto and scrobble_to_teal as docket background tasks
- remove all fallback/bifurcation code - Redis is always required
- simplify background.py (remove is_docket_enabled)
- update auth.py and playback.py to use new schedulers
- add Redis service to CI workflow
- update tests for simplified docket-only flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: update test for schedule_atproto_sync, fix cookies deprecation
- test_list_record_sync: patch schedule_atproto_sync instead of removed sync_atproto_records
- test_hidden_tags_filter: move cookies to client constructor to fix httpx deprecation warning
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: template database pattern for fast parallel test execution
- use template db + clone for xdist workers (instant file copy vs migrations)
- advisory locks coordinate template creation between workers
- patch settings.database.url per-worker for production code compatibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* ci: enable parallel test execution with xdist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: ensure test_app fixture depends on db_session for xdist
fixes race condition where tests using test_app without db_session
would run before database URL was patched for the worker
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add testing philosophy and xdist parallel execution guide
- template database pattern for fast parallel tests
- behavior vs implementation testing philosophy
- common pitfalls with fixture dependencies
- guidance on when private function tests are acceptable
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: migrate ATProto sync and teal scrobbling to docket
- add sync_atproto and scrobble_to_teal as docket background tasks
- remove all fallback/bifurcation code - Redis is always required
- simplify background.py (remove is_docket_enabled)
- update auth.py and playback.py to use new schedulers
- add Redis service to CI workflow
- update tests for simplified docket-only flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: update test for schedule_atproto_sync, fix cookies deprecation
- test_list_record_sync: patch schedule_atproto_sync instead of removed sync_atproto_records
- test_hidden_tags_filter: move cookies to client constructor to fix httpx deprecation warning
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: template database pattern for fast parallel test execution
- use template db + clone for xdist workers (instant file copy vs migrations)
- advisory locks coordinate template creation between workers
- patch settings.database.url per-worker for production code compatibility
- borrowed pattern from prefecthq/nebula
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* ci: enable parallel test execution with xdist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: ensure test_app fixture depends on db_session for xdist
fixes race condition where tests using test_app without db_session
would run before database URL was patched for the worker
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- remove text-transform: lowercase from album card titles on artist page
- add theme-aware scrollbar styling to AddToMenu playlist list
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- move export processing from FastAPI BackgroundTasks to docket
- add process_export task function with full R2 upload logic
- add schedule_export helper with asyncio fallback
- register process_export in docket worker
- add tests for scheduling with docket and asyncio fallback
- update background-tasks.md documentation
exports now benefit from docket's durability: if a worker crashes
mid-export, the task will be retried on restart.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* docs: update env docs for docket and suppressed loggers
- fly.toml: update secrets comments with DOCKET_URL, remove stale entries
- fly.staging.toml: add secrets comments (was missing)
- .env.example: add DOCKET_* and LOGFIRE_SUPPRESSED_LOGGERS settings
- configuration.md: document suppressed_loggers setting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add docket_runs.py script to check task status
standalone uv script to inspect docket runs in redis:
- works with local, staging, or production environments
- no SSH required - connects directly to redis
🤖 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 pydocket for background task infrastructure
- add pydocket dependency for Redis-backed background tasks
- add DocketSettings to config (defaults to memory:// for local dev)
- create background.py with docket initialization and worker lifespan
- create background_tasks.py with scan_copyright task
- migrate copyright scan from asyncio.create_task to docket.add()
- add Redis to test docker-compose
- update AGENTS.md with test command note
the copyright scan is the first task migrated to docket. upload processing
still uses FastAPI BackgroundTasks pending auth session refactoring.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: make docket opt-in, add fallback to asyncio.create_task
- change DOCKET_URL default from "memory://plyr" to "" (disabled)
- add is_docket_enabled() check function
- update background_worker_lifespan() to yield None when disabled
- uploads.py: check is_docket_enabled(), fallback to asyncio.create_task
- remove stub process_upload from background_tasks.py
memory mode won't work in production with multiple machines, so docket
must be explicitly enabled with a Redis URL. when disabled, copyright
scans fall back to fire-and-forget asyncio.create_task().
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: clean up upload background processing
- extract featured artist resolution to handles.py
- extract schedule_copyright_scan to unify docket/asyncio branching
- break _process_upload_background into smaller helper functions:
- _save_audio_to_storage
- _save_image_to_storage
- _add_tags_to_track
- _send_track_notification
- fix sketchy json.JSONDecodeError pass with proper logging
- add compose.yaml for local redis
- add just commands: dev-up, dev-down, dev
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: simplify just dev to single command
removes dev-up/dev-down, just use 'just dev' which starts redis,
backend (with DOCKET_URL set), and frontend.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add background tasks and redis documentation
- add docs/backend/background-tasks.md covering docket/redis setup
- update configuration.md with docket settings
- update environments.md with redis instances per environment
- created Upstash Redis instances:
- plyr-redis-prd (production, iad region)
- plyr-redis-stg (staging, iad region)
note: DOCKET_URL not yet wired to fly secrets - will test in dev first
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: use UploadContext dataclass for background processing
- replace 12 function arguments with single UploadContext dataclass
- cleaner interface for _process_upload_background
- update just commands: dev-services / dev-services-down
- update local dev docs to reflect separate terminal workflow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: reorganize atproto module by lexicon namespace
- split monolithic records.py (1000+ lines) into focused modules:
- client.py: PDS request/auth logic, token refresh with per-session locks
- records/fm_plyr/: plyr.fm lexicons (track, like, comment, list, profile)
- records/fm_teal/: teal.fm lexicons (play, status)
- sync.py: high-level sync orchestration
- maintain backward compatibility via re-exports in records/__init__.py
- update test patch paths for new module locations
- add dev-services commands for local redis via docker compose
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* style: make header stats smaller and evenly distributed
- reduce stats font size (0.75rem → 0.65rem) and icons (14px → 12px)
- distribute stats and search evenly across left margin with space-evenly
- margin width responds correctly when queue panel opens
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: resolve docket startup and logging issues
- rename 'name' to 'docket_name' in log extra dict (conflicts with LogRecord.name)
- suppress docket logger noise by setting level to WARNING
- remove unnecessary sleep(0.1) from worker shutdown
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: make suppressed loggers configurable
- add LOGFIRE_SUPPRESSED_LOGGERS setting (defaults to "docket")
- iterate over setting in main.py instead of hardcoding
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- artist support link feature (PR #532)
- inline playlist editing (PR #531)
- platform stats enhancements (PRs #522, #528)
- navigation data loading fixes (PR #527)
- copyright moderation improvements (PR #480)
- letta-backed status maintenance (PR #529)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add artist support link setting
allows artists to set a support URL (Ko-fi, Patreon, etc.) in their
settings that displays as a button on their public profile page.
- add support_url field to UserPreferences model with migration
- update preferences API to handle support_url get/update
- expose support_url on public artist profile endpoints
- add settings UI with https:// validation
- display support button next to share on artist profiles
- add backend tests for support_url functionality
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: move support link to portal profile section
- moved support URL field from settings page to portal profile section
- renamed "profile settings" to "profile" in portal
- aligned support button with share button on artist profile (32px height)
- cleaned up unused CSS from settings page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: letta-backed status maintenance
replaces claude-code-action with a letta-backed python script that
maintains persistent memory across runs. the agent remembers:
- previous status updates and their content
- architectural decisions and patterns
- recurring themes in development
this means the agent doesn't need to re-read everything from scratch
each week - it builds on accumulated context.
new files:
- scripts/status_maintenance.py - main CI script
- scripts/letta_status_agent.py - local testing/interaction
workflow changes:
- uses uv run scripts/status_maintenance.py instead of claude-code-action
- adds LETTA_API_KEY secret requirement
- adds --dry-run option for testing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use structured outputs and read full STATUS.md
- use anthropic's structured outputs beta (structured-outputs-2025-11-13)
with Pydantic model for guaranteed schema compliance
- read the full STATUS.md content, not truncated to 3000 chars
- remove manual JSON parsing/begging - let the API handle it
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use opus 4.5 by default, make model configurable
- add model setting defaulting to claude-opus-4-5-20251101
- configurable via ANTHROPIC_MODEL env var
- use settings.model everywhere instead of hardcoded sonnet
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: memory blocks for project understanding, restore full prompt guidance
memory blocks refactored:
- project_architecture: how plyr.fm is built, key design decisions
- atproto_context: understanding of ATProto, lexicons, NSIDs
- recurring_patterns: themes that come up repeatedly
the agent no longer tracks 'what it processed' - github (last merged PR)
is the source of truth for the time window. memory is purely for
accumulating project understanding.
restored all original prompt guidance:
- full narrative structure (opening, main story, secondary, rapid fire, closing)
- tone requirements (skeptical, amused, no superlatives)
- pronunciation rules (player FM, never plyr.fm)
- time reference rules (specific dates, never 'last week')
- identifying what shipped vs improved
- first episode detection and longer script guidance
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: tighten system prompt (-75 lines)
preserve critical guidance:
- pronunciation (player FM)
- time bounds (specific dates)
- narrative structure
- tone (dry, sardonic)
remove redundancy:
- consolidated CRITICAL markers
- merged duplicate tone sections
- simplified structure timing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add unified edit mode for playlists with track deletion
- Consolidate edit functionality into single edit mode (like Spotify)
- Edit button now toggles edit mode instead of opening modal
- In edit mode: show drag handles for reordering, delete buttons for tracks, and edit details button
- Remove separate reorder button - reordering is now part of edit mode
- Add track deletion UI with loading states and error handling
- Track deletion uses existing backend API endpoint
* fix: simplify playlist edit mode UX
- fix undefined showEdit variable errors (use showEditModal)
- simplify edit mode: one button to enter/exit edit mode
- in edit mode: cover art is clickable to edit metadata, tracks show
drag handles and delete buttons, add tracks button appears inline
- remove redundant "edit details", "add tracks", "saving..." buttons
- use proper button element for clickable cover art (fixes a11y warning)
- clean up unused CSS selectors
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: inline playlist editing with direct cover upload
- title editable inline as input field when in edit mode
- cover art directly replaceable via click (opens file picker)
- removed edit modal entirely - all editing is now inline
- fixed font inheritance on overlay text
- cleaned up unused CSS selectors from removed modal
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Stats and search now in one flex container, centered in left margin
- Mirrors how logout is centered in right margin
- Container has max-width to prevent overflow into content area
- Stats can wrap to 2 rows if numbers get large (flex-wrap)
- Smaller font size (0.75rem) for stats to fit better
- Breakpoint at 1599px to account for queue panel
* fix: reload data when navigating between detail pages of same type
SvelteKit reuses component instances when navigating between routes with
the same layout. This meant `onMount` didn't re-run when going from one
artist page to another, or one track page to another.
- replace `onMount` with `$effect` watching server data (data.artist.did,
data.track.id) to detect navigation
- reset local state and reload fresh data when route params change
- also: change "of music" to "of audio" in platform stats (not all
uploads are music - some are audiobooks, public domain recordings, etc)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add Tangled repo link and fix responsive header breakpoints
- Add Tangled link with sheep avatar to desktop header and mobile LinksMenu
- Consolidate breakpoints: desktop elements and mobile layout now switch at 1299px
- Previously stats/search/logout disappeared at 1499px but mobile didn't show until 1299px,
leaving a gap where users couldn't access those features
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: bump responsive breakpoint to 1399px to prevent stats/search collision
Switch to mobile layout earlier to avoid cramped margin elements
* fix: accent color on hover for nav-link and Tangled icon
- nav-link (feed/library) now uses accent color on hover
- Tangled icon gets accent-colored ring on hover
- Nudge search component 20px right to prevent overlap with stats
---------
Co-authored-by: Claude <noreply@anthropic.com>
- add `total_duration_seconds` to platform stats endpoint (`/stats`)
- add `total_duration_seconds` to artist analytics endpoint (`/artists/{did}/analytics`)
- display duration in homepage stats bar (header and menu variants)
- show duration as subtitle in artist page "total tracks" card
- add `formatDuration()` helper for human-readable format (e.g., "25h 32m")
- add `scripts/user_upload_stats.py` for viewing per-user upload durations
- add regression tests for stats and analytics endpoints
this lays groundwork for future upload caps per user.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: sync copyright flag resolution from labeler to portal
when an admin marks a track as "false positive" in the moderation UI,
the flag now correctly clears from the user's portal. previously the
backend's copyright_scans table was never updated, leaving stale flags.
the fix uses an idempotent approach - the backend now queries the
labeler for the source of truth rather than requiring a sync/backfill:
moderation service:
- add POST /admin/active-labels endpoint
- returns which URIs have active (non-negated) copyright-violation labels
backend:
- add get_active_copyright_labels() to query the labeler
- update get_copyright_info() to check labeler for pending flags
- lazily update resolution field when labeler confirms resolution
- skip labeler call for already-resolved scans (optimization)
behavior:
- existing resolved flags immediately take effect (no backfill needed)
- fails closed: if labeler unreachable, flags remain visible (safe default)
- lazy DB update reduces future labeler calls for same track
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add copyright attestation checkbox to upload flow
users must now confirm they have distribution rights before uploading.
includes educational copy about "publicly available ≠ licensed" to reduce
good-faith mistakes (per tigers blood incident retrospective).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- backend expects tags as JSON arrays (`["ai"]`), not bare strings
- check existing tracks via `plyrfm my-tracks` before uploading
- if a track for today already exists, add episode number (#2, #3, etc)
🤖 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[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
- switch from sonnet (default) to opus for higher quality output
- add Task tool to enable subagent investigation of PRs
- restructure podcast script generation to:
- deeply investigate PRs before writing (read bodies, diffs, design decisions)
- categorize changes into big ticket items vs smaller fixes
- follow explicit chronological narrative structure:
- opening (10s)
- main story with design discussion (60-90s)
- secondary feature (30-45s)
- rapid fire smaller changes (20-30s)
- closing (10s)
- emphasize explaining HOW things were designed, not just WHAT
closes #521
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add CLAUDE.md with instructions for getting workflow summary URLs
- fix pronunciation: tell model to WRITE "player FM" not "plyr.fm" (TTS mispronounces plyr)
- add guidance to distinguish major feature launches from polish/fixes
- explicitly forbid vague time references like "last week" - require specific dates
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add get_record_public() to _internal/atproto/records.py for unauthenticated ATProto record fetches
- remove auth requirement from GET /playlists/{id} endpoint
- remove auth redirect from frontend playlist page loader
- edit/delete buttons still only show for playlist owner (via client-side auth check)
ATProto records are public by design - the previous auth requirement was unnecessary for read access.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the workflow was using $(date +%Y%m%d) for branch names, causing
collisions when run multiple times on the same day. this led to
the audio file not being committed in PR #516.
now uses $(date +%Y%m%d-%H%M%S) stored in a variable to ensure
unique branch names and consistent usage across checkout and push.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: detail page button layouts
playlist detail page:
- show edit/delete buttons on mobile (were hidden, only share showed)
- rename mobile-share-button to mobile-buttons for clarity
track detail page:
- remove bifurcated desktop layout (buttons on left/right sides)
- use centered button layout everywhere (like mobile had)
- simplify CSS by removing side-button-left/right styles
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: AddToMenu opens upward when near bottom of viewport
when the menu trigger is close to the bottom of the viewport,
detect available space and open the menu upward instead of downward.
this prevents the menu from being cut off by the player.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
living documentation explaining:
- what lexicons are and how ATProto uses them
- our fm.plyr namespace and environment awareness
- each lexicon (track, like, comment, list, profile) with history
- ATProto primitives we use (tid, literal:self, strongRef, knownValues)
- local indexing pattern for performance
- future codegen plans (issue #494)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the workflow was using "last week" as its time window, which caused it
to miss work that happened between the last merged PR and ~7 days ago.
changes:
- query gh pr list to find when last status-maintenance PR was MERGED
- filter by branch name (startswith "status-maintenance-") and sort by mergedAt
- use that merge date as the starting point, not "last week"
- clarify that archive files are organized by month (YYYY-MM.md)
- add examples for archive file naming (2025-12.md for December, etc.)
- remove hardcoded "weekly" titles, let Claude craft descriptive titles
fixes the issue where PR #512 only covered Dec 7th onwards when it
should have covered everything since Dec 2nd (last merge date).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
documents the fast-follow fixes after the playlists release:
- PR #507: playlist og:image link previews
- PR #508: auth invalidation after login
- PR #509: playlist menu fixes and link previews
- PR #510: inline playlist creation to avoid playback interruption
also updates the playlists section to reflect that create playlist is now
inline rather than navigation-based.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the create playlist link was navigating to /library?create=playlist, which
caused the layout to reinitialize and destroy the audio element. this fix
adds inline playlist creation to AddToMenu and TrackActionsMenu, allowing
users to create playlists and add tracks without leaving the current page.
changes:
- AddToMenu: replace link with inline create form that creates playlist
and adds track in one action
- TrackActionsMenu: same inline create form treatment
- portal: update empty state link to just go to /library (no query param)
- library page: remove query param handling (no longer needed)
- library +page.ts: use SvelteKit's fetch instead of window.fetch
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- fix stopPropagation blocking link clicks in AddToMenu and TrackActionsMenu
- add /playlist/ to hasPageMetadata so layout default OG tags don't override
- replace LikeButton with AddToMenu on track detail page for playlist access
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
after exchanging the OAuth token for a session cookie, the root
layout's load function still has stale data (isAuthenticated: false)
from before the cookie was set.
this caused navigation to /library immediately after login to redirect
to / because the layout data said the user wasn't authenticated.
the fix: call invalidateAll() after successful token exchange so
SvelteKit reruns all load functions with the new session cookie.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the SSR fallback object was missing image_url from playlistMeta,
causing og:image to be undefined in link previews even when the
playlist has cover art.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>
🤖 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>
Update album title in UI immediately instead of blocking on backend
response. Toast appears when backend completes, and UI reverts on error.
This makes the edit feel much snappier since the backend call can take
several seconds (updating all track ATProto records + list record).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
When an album title is updated via PATCH /albums/{album_id}, we now also
update the album's ATProto list record with the new name. Previously only
track records were updated, leaving the list record with the stale name.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: album delete endpoint and track ordering from ATProto list
fixes two user-reported issues:
1. album deletion: adds DELETE /albums/{album_id} endpoint
- by default orphans tracks (sets album_id to null)
- with ?cascade=true deletes all tracks in the album
- cleans up ATProto list record and cover image
2. album track ordering: respects ATProto list record order
- get_album now fetches the ATProto list record to determine track order
- falls back to created_at order if no ATProto record exists
- fixes issue where reordering tracks in the frontend didn't persist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: album edit mode with ATProto sync and portal UX improvements
- add PATCH /albums/{album_id} endpoint for title/description updates
- sync album title changes to all track ATProto records
- add DELETE /albums/{album_id}/tracks/{track_id} to remove tracks from albums
- implement album page edit mode (inline title, cover upload, delete album)
- simplify portal albums section to clickable links (matches playlist UX)
- fix icon buttons to use square shape matching playlist page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- created plyr-stats bucket with public dev-url access
- costs data now lives in dedicated bucket, shared across all envs
- removes dependency on environment-specific audio buckets
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: public costs dashboard with daily export to R2
- add /costs page showing monthly infrastructure costs
- costs.json exported daily via GitHub Action to R2
- backend /config endpoint exposes costs_json_url
- ko-fi support link for community funding
- hardcoded Fly/Neon/Cloudflare costs, dynamic AudD from DB
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: correct audd cost calculation with one-time adjustment
the copyright_scans table was created Nov 24 but first scan recorded
Nov 30, so database counts are incomplete for this billing period.
- hardcode audd values for Nov 24 - Dec 24 from dashboard (6781 requests, $3.91)
- after Dec 24, automatically uses live database counts
- includes cleanup comment to remove hardcoded block after transition
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update STATUS.md - cost dashboard complete
- add public cost dashboard to recent work section
- remove from immediate priorities (done)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* docs: audit and update documentation
- promote connection-pool-exhaustion runbook from sandbox to docs/runbooks/
- update CLAUDE.md files with current module descriptions
- _internal: add background tasks, moderation, jobs
- api: add albums, playlists, exports, moderation, stats
- docs: add runbooks, testing, moderation, lexicons
- refresh docs/README.md with streamlined navigation
- update STATUS.md:
- add Dec 9 docket/concurrent exports section
- update immediate priorities (moderation cleanup, cost dashboard)
- de-emphasize transcoder (moved to backlog)
- add docket to technical architecture
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update README with current state
- add docket, logfire to tech stack
- add moderation/transcoder services section
- update local dev to include dev-services (redis)
- add useful commands section with actual justfile targets
- update features (playlists, scrobbling, timed comments, support links)
- add data ownership section
- update project structure (_internal services)
- consolidate links + documentation into single section
- add mirrors section (github, tangled)
- remove stale atproto fork reference
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
terminal dashboard for monitoring AudD copyright scan API usage
and costs. tracks scans per billing period (24th of month), shows
remaining free requests, estimated costs, and daily breakdown with
plotext charts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
previously tracks were downloaded sequentially, making exports slow
for users with many tracks or large files. now downloads happen
concurrently (up to 4 at a time) while zip creation remains sequential
(zipfile isn't thread-safe).
- refactor process_export to use asyncio.gather + semaphore
- add regression test verifying concurrent download behavior
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: migrate ATProto sync and teal scrobbling to docket
- add sync_atproto and scrobble_to_teal as docket background tasks
- remove all fallback/bifurcation code - Redis is always required
- simplify background.py (remove is_docket_enabled)
- update auth.py and playback.py to use new schedulers
- add Redis service to CI workflow
- update tests for simplified docket-only flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: update test for schedule_atproto_sync, fix cookies deprecation
- test_list_record_sync: patch schedule_atproto_sync instead of removed sync_atproto_records
- test_hidden_tags_filter: move cookies to client constructor to fix httpx deprecation warning
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: template database pattern for fast parallel test execution
- use template db + clone for xdist workers (instant file copy vs migrations)
- advisory locks coordinate template creation between workers
- patch settings.database.url per-worker for production code compatibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* ci: enable parallel test execution with xdist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: ensure test_app fixture depends on db_session for xdist
fixes race condition where tests using test_app without db_session
would run before database URL was patched for the worker
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add testing philosophy and xdist parallel execution guide
- template database pattern for fast parallel tests
- behavior vs implementation testing philosophy
- common pitfalls with fixture dependencies
- guidance on when private function tests are acceptable
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: migrate ATProto sync and teal scrobbling to docket
- add sync_atproto and scrobble_to_teal as docket background tasks
- remove all fallback/bifurcation code - Redis is always required
- simplify background.py (remove is_docket_enabled)
- update auth.py and playback.py to use new schedulers
- add Redis service to CI workflow
- update tests for simplified docket-only flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: update test for schedule_atproto_sync, fix cookies deprecation
- test_list_record_sync: patch schedule_atproto_sync instead of removed sync_atproto_records
- test_hidden_tags_filter: move cookies to client constructor to fix httpx deprecation warning
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: template database pattern for fast parallel test execution
- use template db + clone for xdist workers (instant file copy vs migrations)
- advisory locks coordinate template creation between workers
- patch settings.database.url per-worker for production code compatibility
- borrowed pattern from prefecthq/nebula
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* ci: enable parallel test execution with xdist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: ensure test_app fixture depends on db_session for xdist
fixes race condition where tests using test_app without db_session
would run before database URL was patched for the worker
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- move export processing from FastAPI BackgroundTasks to docket
- add process_export task function with full R2 upload logic
- add schedule_export helper with asyncio fallback
- register process_export in docket worker
- add tests for scheduling with docket and asyncio fallback
- update background-tasks.md documentation
exports now benefit from docket's durability: if a worker crashes
mid-export, the task will be retried on restart.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* docs: update env docs for docket and suppressed loggers
- fly.toml: update secrets comments with DOCKET_URL, remove stale entries
- fly.staging.toml: add secrets comments (was missing)
- .env.example: add DOCKET_* and LOGFIRE_SUPPRESSED_LOGGERS settings
- configuration.md: document suppressed_loggers setting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add docket_runs.py script to check task status
standalone uv script to inspect docket runs in redis:
- works with local, staging, or production environments
- no SSH required - connects directly to redis
🤖 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 pydocket for background task infrastructure
- add pydocket dependency for Redis-backed background tasks
- add DocketSettings to config (defaults to memory:// for local dev)
- create background.py with docket initialization and worker lifespan
- create background_tasks.py with scan_copyright task
- migrate copyright scan from asyncio.create_task to docket.add()
- add Redis to test docker-compose
- update AGENTS.md with test command note
the copyright scan is the first task migrated to docket. upload processing
still uses FastAPI BackgroundTasks pending auth session refactoring.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: make docket opt-in, add fallback to asyncio.create_task
- change DOCKET_URL default from "memory://plyr" to "" (disabled)
- add is_docket_enabled() check function
- update background_worker_lifespan() to yield None when disabled
- uploads.py: check is_docket_enabled(), fallback to asyncio.create_task
- remove stub process_upload from background_tasks.py
memory mode won't work in production with multiple machines, so docket
must be explicitly enabled with a Redis URL. when disabled, copyright
scans fall back to fire-and-forget asyncio.create_task().
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: clean up upload background processing
- extract featured artist resolution to handles.py
- extract schedule_copyright_scan to unify docket/asyncio branching
- break _process_upload_background into smaller helper functions:
- _save_audio_to_storage
- _save_image_to_storage
- _add_tags_to_track
- _send_track_notification
- fix sketchy json.JSONDecodeError pass with proper logging
- add compose.yaml for local redis
- add just commands: dev-up, dev-down, dev
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: simplify just dev to single command
removes dev-up/dev-down, just use 'just dev' which starts redis,
backend (with DOCKET_URL set), and frontend.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add background tasks and redis documentation
- add docs/backend/background-tasks.md covering docket/redis setup
- update configuration.md with docket settings
- update environments.md with redis instances per environment
- created Upstash Redis instances:
- plyr-redis-prd (production, iad region)
- plyr-redis-stg (staging, iad region)
note: DOCKET_URL not yet wired to fly secrets - will test in dev first
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: use UploadContext dataclass for background processing
- replace 12 function arguments with single UploadContext dataclass
- cleaner interface for _process_upload_background
- update just commands: dev-services / dev-services-down
- update local dev docs to reflect separate terminal workflow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: reorganize atproto module by lexicon namespace
- split monolithic records.py (1000+ lines) into focused modules:
- client.py: PDS request/auth logic, token refresh with per-session locks
- records/fm_plyr/: plyr.fm lexicons (track, like, comment, list, profile)
- records/fm_teal/: teal.fm lexicons (play, status)
- sync.py: high-level sync orchestration
- maintain backward compatibility via re-exports in records/__init__.py
- update test patch paths for new module locations
- add dev-services commands for local redis via docker compose
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* style: make header stats smaller and evenly distributed
- reduce stats font size (0.75rem → 0.65rem) and icons (14px → 12px)
- distribute stats and search evenly across left margin with space-evenly
- margin width responds correctly when queue panel opens
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: resolve docket startup and logging issues
- rename 'name' to 'docket_name' in log extra dict (conflicts with LogRecord.name)
- suppress docket logger noise by setting level to WARNING
- remove unnecessary sleep(0.1) from worker shutdown
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: make suppressed loggers configurable
- add LOGFIRE_SUPPRESSED_LOGGERS setting (defaults to "docket")
- iterate over setting in main.py instead of hardcoding
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- artist support link feature (PR #532)
- inline playlist editing (PR #531)
- platform stats enhancements (PRs #522, #528)
- navigation data loading fixes (PR #527)
- copyright moderation improvements (PR #480)
- letta-backed status maintenance (PR #529)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add artist support link setting
allows artists to set a support URL (Ko-fi, Patreon, etc.) in their
settings that displays as a button on their public profile page.
- add support_url field to UserPreferences model with migration
- update preferences API to handle support_url get/update
- expose support_url on public artist profile endpoints
- add settings UI with https:// validation
- display support button next to share on artist profiles
- add backend tests for support_url functionality
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: move support link to portal profile section
- moved support URL field from settings page to portal profile section
- renamed "profile settings" to "profile" in portal
- aligned support button with share button on artist profile (32px height)
- cleaned up unused CSS from settings page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: letta-backed status maintenance
replaces claude-code-action with a letta-backed python script that
maintains persistent memory across runs. the agent remembers:
- previous status updates and their content
- architectural decisions and patterns
- recurring themes in development
this means the agent doesn't need to re-read everything from scratch
each week - it builds on accumulated context.
new files:
- scripts/status_maintenance.py - main CI script
- scripts/letta_status_agent.py - local testing/interaction
workflow changes:
- uses uv run scripts/status_maintenance.py instead of claude-code-action
- adds LETTA_API_KEY secret requirement
- adds --dry-run option for testing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use structured outputs and read full STATUS.md
- use anthropic's structured outputs beta (structured-outputs-2025-11-13)
with Pydantic model for guaranteed schema compliance
- read the full STATUS.md content, not truncated to 3000 chars
- remove manual JSON parsing/begging - let the API handle it
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use opus 4.5 by default, make model configurable
- add model setting defaulting to claude-opus-4-5-20251101
- configurable via ANTHROPIC_MODEL env var
- use settings.model everywhere instead of hardcoded sonnet
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: memory blocks for project understanding, restore full prompt guidance
memory blocks refactored:
- project_architecture: how plyr.fm is built, key design decisions
- atproto_context: understanding of ATProto, lexicons, NSIDs
- recurring_patterns: themes that come up repeatedly
the agent no longer tracks 'what it processed' - github (last merged PR)
is the source of truth for the time window. memory is purely for
accumulating project understanding.
restored all original prompt guidance:
- full narrative structure (opening, main story, secondary, rapid fire, closing)
- tone requirements (skeptical, amused, no superlatives)
- pronunciation rules (player FM, never plyr.fm)
- time reference rules (specific dates, never 'last week')
- identifying what shipped vs improved
- first episode detection and longer script guidance
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: tighten system prompt (-75 lines)
preserve critical guidance:
- pronunciation (player FM)
- time bounds (specific dates)
- narrative structure
- tone (dry, sardonic)
remove redundancy:
- consolidated CRITICAL markers
- merged duplicate tone sections
- simplified structure timing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add unified edit mode for playlists with track deletion
- Consolidate edit functionality into single edit mode (like Spotify)
- Edit button now toggles edit mode instead of opening modal
- In edit mode: show drag handles for reordering, delete buttons for tracks, and edit details button
- Remove separate reorder button - reordering is now part of edit mode
- Add track deletion UI with loading states and error handling
- Track deletion uses existing backend API endpoint
* fix: simplify playlist edit mode UX
- fix undefined showEdit variable errors (use showEditModal)
- simplify edit mode: one button to enter/exit edit mode
- in edit mode: cover art is clickable to edit metadata, tracks show
drag handles and delete buttons, add tracks button appears inline
- remove redundant "edit details", "add tracks", "saving..." buttons
- use proper button element for clickable cover art (fixes a11y warning)
- clean up unused CSS selectors
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: inline playlist editing with direct cover upload
- title editable inline as input field when in edit mode
- cover art directly replaceable via click (opens file picker)
- removed edit modal entirely - all editing is now inline
- fixed font inheritance on overlay text
- cleaned up unused CSS selectors from removed modal
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Stats and search now in one flex container, centered in left margin
- Mirrors how logout is centered in right margin
- Container has max-width to prevent overflow into content area
- Stats can wrap to 2 rows if numbers get large (flex-wrap)
- Smaller font size (0.75rem) for stats to fit better
- Breakpoint at 1599px to account for queue panel
* fix: reload data when navigating between detail pages of same type
SvelteKit reuses component instances when navigating between routes with
the same layout. This meant `onMount` didn't re-run when going from one
artist page to another, or one track page to another.
- replace `onMount` with `$effect` watching server data (data.artist.did,
data.track.id) to detect navigation
- reset local state and reload fresh data when route params change
- also: change "of music" to "of audio" in platform stats (not all
uploads are music - some are audiobooks, public domain recordings, etc)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add Tangled repo link and fix responsive header breakpoints
- Add Tangled link with sheep avatar to desktop header and mobile LinksMenu
- Consolidate breakpoints: desktop elements and mobile layout now switch at 1299px
- Previously stats/search/logout disappeared at 1499px but mobile didn't show until 1299px,
leaving a gap where users couldn't access those features
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: bump responsive breakpoint to 1399px to prevent stats/search collision
Switch to mobile layout earlier to avoid cramped margin elements
* fix: accent color on hover for nav-link and Tangled icon
- nav-link (feed/library) now uses accent color on hover
- Tangled icon gets accent-colored ring on hover
- Nudge search component 20px right to prevent overlap with stats
---------
Co-authored-by: Claude <noreply@anthropic.com>
- add `total_duration_seconds` to platform stats endpoint (`/stats`)
- add `total_duration_seconds` to artist analytics endpoint (`/artists/{did}/analytics`)
- display duration in homepage stats bar (header and menu variants)
- show duration as subtitle in artist page "total tracks" card
- add `formatDuration()` helper for human-readable format (e.g., "25h 32m")
- add `scripts/user_upload_stats.py` for viewing per-user upload durations
- add regression tests for stats and analytics endpoints
this lays groundwork for future upload caps per user.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: sync copyright flag resolution from labeler to portal
when an admin marks a track as "false positive" in the moderation UI,
the flag now correctly clears from the user's portal. previously the
backend's copyright_scans table was never updated, leaving stale flags.
the fix uses an idempotent approach - the backend now queries the
labeler for the source of truth rather than requiring a sync/backfill:
moderation service:
- add POST /admin/active-labels endpoint
- returns which URIs have active (non-negated) copyright-violation labels
backend:
- add get_active_copyright_labels() to query the labeler
- update get_copyright_info() to check labeler for pending flags
- lazily update resolution field when labeler confirms resolution
- skip labeler call for already-resolved scans (optimization)
behavior:
- existing resolved flags immediately take effect (no backfill needed)
- fails closed: if labeler unreachable, flags remain visible (safe default)
- lazy DB update reduces future labeler calls for same track
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add copyright attestation checkbox to upload flow
users must now confirm they have distribution rights before uploading.
includes educational copy about "publicly available ≠ licensed" to reduce
good-faith mistakes (per tigers blood incident retrospective).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- backend expects tags as JSON arrays (`["ai"]`), not bare strings
- check existing tracks via `plyrfm my-tracks` before uploading
- if a track for today already exists, add episode number (#2, #3, etc)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- switch from sonnet (default) to opus for higher quality output
- add Task tool to enable subagent investigation of PRs
- restructure podcast script generation to:
- deeply investigate PRs before writing (read bodies, diffs, design decisions)
- categorize changes into big ticket items vs smaller fixes
- follow explicit chronological narrative structure:
- opening (10s)
- main story with design discussion (60-90s)
- secondary feature (30-45s)
- rapid fire smaller changes (20-30s)
- closing (10s)
- emphasize explaining HOW things were designed, not just WHAT
closes #521
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add CLAUDE.md with instructions for getting workflow summary URLs
- fix pronunciation: tell model to WRITE "player FM" not "plyr.fm" (TTS mispronounces plyr)
- add guidance to distinguish major feature launches from polish/fixes
- explicitly forbid vague time references like "last week" - require specific dates
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add get_record_public() to _internal/atproto/records.py for unauthenticated ATProto record fetches
- remove auth requirement from GET /playlists/{id} endpoint
- remove auth redirect from frontend playlist page loader
- edit/delete buttons still only show for playlist owner (via client-side auth check)
ATProto records are public by design - the previous auth requirement was unnecessary for read access.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the workflow was using $(date +%Y%m%d) for branch names, causing
collisions when run multiple times on the same day. this led to
the audio file not being committed in PR #516.
now uses $(date +%Y%m%d-%H%M%S) stored in a variable to ensure
unique branch names and consistent usage across checkout and push.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: detail page button layouts
playlist detail page:
- show edit/delete buttons on mobile (were hidden, only share showed)
- rename mobile-share-button to mobile-buttons for clarity
track detail page:
- remove bifurcated desktop layout (buttons on left/right sides)
- use centered button layout everywhere (like mobile had)
- simplify CSS by removing side-button-left/right styles
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: AddToMenu opens upward when near bottom of viewport
when the menu trigger is close to the bottom of the viewport,
detect available space and open the menu upward instead of downward.
this prevents the menu from being cut off by the player.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
living documentation explaining:
- what lexicons are and how ATProto uses them
- our fm.plyr namespace and environment awareness
- each lexicon (track, like, comment, list, profile) with history
- ATProto primitives we use (tid, literal:self, strongRef, knownValues)
- local indexing pattern for performance
- future codegen plans (issue #494)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the workflow was using "last week" as its time window, which caused it
to miss work that happened between the last merged PR and ~7 days ago.
changes:
- query gh pr list to find when last status-maintenance PR was MERGED
- filter by branch name (startswith "status-maintenance-") and sort by mergedAt
- use that merge date as the starting point, not "last week"
- clarify that archive files are organized by month (YYYY-MM.md)
- add examples for archive file naming (2025-12.md for December, etc.)
- remove hardcoded "weekly" titles, let Claude craft descriptive titles
fixes the issue where PR #512 only covered Dec 7th onwards when it
should have covered everything since Dec 2nd (last merge date).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
documents the fast-follow fixes after the playlists release:
- PR #507: playlist og:image link previews
- PR #508: auth invalidation after login
- PR #509: playlist menu fixes and link previews
- PR #510: inline playlist creation to avoid playback interruption
also updates the playlists section to reflect that create playlist is now
inline rather than navigation-based.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the create playlist link was navigating to /library?create=playlist, which
caused the layout to reinitialize and destroy the audio element. this fix
adds inline playlist creation to AddToMenu and TrackActionsMenu, allowing
users to create playlists and add tracks without leaving the current page.
changes:
- AddToMenu: replace link with inline create form that creates playlist
and adds track in one action
- TrackActionsMenu: same inline create form treatment
- portal: update empty state link to just go to /library (no query param)
- library page: remove query param handling (no longer needed)
- library +page.ts: use SvelteKit's fetch instead of window.fetch
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- fix stopPropagation blocking link clicks in AddToMenu and TrackActionsMenu
- add /playlist/ to hasPageMetadata so layout default OG tags don't override
- replace LikeButton with AddToMenu on track detail page for playlist access
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
after exchanging the OAuth token for a session cookie, the root
layout's load function still has stale data (isAuthenticated: false)
from before the cookie was set.
this caused navigation to /library immediately after login to redirect
to / because the layout data said the user wasn't authenticated.
the fix: call invalidateAll() after successful token exchange so
SvelteKit reruns all load functions with the new session cookie.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>