commits
Was blocking all Bluesky avatars. Keep only explicit flagging.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Fix regex to match R2 URLs (pub-*.r2.dev/{id}.{ext})
- Blur non-R2 images by default as they could be injected via ATProto records
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: blur explicit images with user opt-in to show
- add explicit_images table to track flagged image URLs/IDs
- add show_explicit_artwork user preference (default: hidden)
- add /moderation/explicit-images endpoint to fetch flagged images
- add ExplicitImage component that blurs flagged images
- hide explicit images from og:image/twitter meta tags
- add portal toggle for explicit artwork preference
images can be flagged by image_id (R2) or full URL (external avatars).
frontend fetches the list once and checks all rendered images.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: use 'sensitive' instead of 'explicit' in tooltip
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: rename explicit to sensitive throughout codebase
- Rename table: explicit_images → sensitive_images
- Rename model: ExplicitImage → SensitiveImage
- Rename preference: show_explicit_artwork → show_sensitive_artwork
- Rename component: ExplicitImage.svelte → SensitiveImage.svelte
- Update all references, endpoints, and UI text
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
added note about intermittent audio playback hangs in iOS standalone
(PWA) mode. PR #466 added NetworkOnly caching for audio routes, but
iOS Safari is slow to update service workers. workaround is to delete
and re-add home screen bookmark. flagged for further investigation if
issue persists.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Space: play/pause
- Left/Right arrows: seek ±10 seconds
- J/L: previous/next track
- M: mute/unmute
All shortcuts respect input focus and are disabled when search modal is open.
Also fixes AGENTS.md to reflect that STATUS.md is now tracked.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
terminal-based vanity metrics dashboard using plotext for visualization.
features:
- zone stats: requests, pageviews, unique visitors, bandwidth, cache ratio
- daily requests bar chart (cyan)
- pageviews vs uniques line chart (green/magenta)
- CLI options: --days/-d for time window, --no-cache to force refresh
- automatic daily caching (~/.cache/plyr-analytics/)
- pydantic-settings for .env integration (CF_API_TOKEN, CF_ZONE_ID)
usage:
uv run scripts/cf_analytics.py # last 7 days
uv run scripts/cf_analytics.py -d 30 # last 30 days
uv run scripts/cf_analytics.py --no-cache
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the $effect that calls incrementPlayCount() runs on every currentTime
update (~4x/second). when threshold is first crossed, svelte's batched
reactive updates meant the guard (playCountedForTrack === currentTrack.id)
could miss rapid-fire calls, causing 2 scrobbles ~50-125ms apart.
fix: add synchronous (non-reactive) guard that blocks immediately,
before the async fetch fires.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add mutagen dependency for audio metadata extraction
- create backend/utilities/audio.py with extract_duration()
- extract duration during upload, store in track.extra["duration"]
- add backfill script for tracks uploaded before this feature
duration is now correctly passed to teal.fm scrobble records.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add Dec 4 section for PR #467 (teal.fm scrobbling)
- document lexicons, configuration, and code quality improvements
- update now-playing section to reflect native scrobbling
- add teal.fm to "what's working" list
- update last modified date
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* checkpoint
* checkpoint
* feat: teal.fm scrobbling integration
adds teal.fm scrobbling support - when users enable the toggle in settings
and re-authenticate, their plays are recorded to their PDS using teal's
ATProto lexicons.
changes:
- add TealSettings config class for configurable namespaces (TEAL_PLAY_COLLECTION,
TEAL_STATUS_COLLECTION, TEAL_ENABLED env vars)
- fix play count fetch missing credentials: 'include' (root cause of scrobbles
not triggering)
- add preferences.fetch() after login to ensure teal toggle state is current
- add unit tests for env var overrides (proves we can adapt when teal graduates
from alpha namespace)
the teal namespace defaults to fm.teal.alpha.* but can be changed via env vars
when teal.fm updates their lexicons.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: code quality improvements and bug fixes
- walrus operators throughout auth and playback code
- extract bearer token parsing into utilities/auth.py
- move DeveloperTokenInfo to top of file with field_validator for truncation
- fix now-playing firing every 1s (update lastReportedState in scheduled reports)
- use settings.frontend.url/domain for origin URLs (environment-aware)
- move teal.fm scrobbling setting from gear menu to portal "Your Data" section
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
audio streaming was intermittently hanging in iOS standalone (PWA) mode.
the service worker was intercepting /audio/{file_id} requests with
NetworkFirst caching, which caused issues with:
- 307 redirects to R2 CDN getting cached/stale
- range request headers not being handled properly on iOS Safari
switched to NetworkOnly for audio routes so the SW passes through
without interference. we weren't actually caching audio files anyway
(just the redirect response), so there's no functional loss.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
The logout button was rendered outside the isAuthenticated check,
causing both login and logout buttons to appear on desktop when
logged out.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
The search modal was using `visibility: hidden` to hide the backdrop,
which prevented the input from receiving focus. This caused:
- No text cursor on desktop when pressing Cmd+K
- No keyboard popup on mobile when tapping search icon
Changed to use only `opacity: 0` (already present) for hiding.
Elements with `opacity: 0` remain focusable, allowing the synchronous
focus() call in search.open() to work correctly - critical for mobile
keyboards which only open when focus is in the same call stack as the
user gesture.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Removes debug logging that was left in from initial development in PR #233.
Keeps console.error for actual error handling.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
uv run by default syncs all dependencies including dev deps, which
caused the release command to hang downloading ruff, jedi, etc.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the now-playing endpoints were hitting the global 100/minute rate limit
when multiple users/tabs were active simultaneously. since these endpoints
are already throttled client-side (10-second intervals, 1-second debounce,
5-second progress buckets), server-side rate limiting is unnecessary.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the documented commands didn't match actual justfile recipes:
- `just frontend install` -> doesn't exist
- `just run-backend` -> `just backend run`
- `just lint` -> `just backend lint` / `just frontend check`
- `just migrate` -> `just backend migrate`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
when a track doesn't have its own artwork but belongs to an album
with artwork, use the album artwork instead of the placeholder icon.
applies to:
- player artwork thumbnail (TrackInfo.svelte)
- media session metadata for system controls (Player.svelte)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
iOS/mobile browsers only open keyboard when focus() is called directly
in a user gesture handler. Previously, focus happened in a Svelte $effect
after state change, which broke the gesture chain.
Fix: Always render SearchModal (hidden via CSS), register input ref with
search state, and focus directly in search.open() before state change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: move search to top-level mobile header
- Add search icon button to mobile-center nav (left of feed/liked icons)
- Remove search from ProfileMenu dropdown
- Space icons evenly across mobile header
- Cleaner mobile UX with one less tap to search
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: hide keyboard hints on mobile, smaller placeholder text
- Hide Cmd+K shortcut badge on mobile
- Hide keyboard navigation hints (arrows, esc) on mobile
- Shrink placeholder text on mobile to prevent overflow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: collapse excess tags with +N button and fix mobile menu overlap
- limits visible tags to 2 by default (never wraps to second line)
- shows "+N" button when more tags exist
- clicking "+N" expands to show all tags (allows wrapping when expanded)
- uses flex-wrap: nowrap + overflow: hidden to guarantee single-line constraint
- collapses back when track changes (component recycle)
- fix LinksMenu and ProfileMenu mobile positioning to avoid player overlap
- menus now position from bottom with player height offset on mobile
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: keep mobile menus centered but shift up for player
- menus stay full-size and centered
- shift center point up by half player height when player is open
- cap max-height to avoid overlap with player
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add search trigger to header and mobile menu
desktop:
- SearchTrigger component shows magnifying glass + keyboard shortcut hint
- detects platform for correct shortcut (⌘K on Mac, Ctrl+K on Windows/Linux)
- subtle styling with accent highlight on hover
mobile:
- search added as first item in ProfileMenu (three-dot menu)
- styled with accent tint background to stand out
- opens search modal and closes menu
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: hide portal link on portal page, move search to far right
- portal link (@handle) now hidden when already on /portal
- search trigger moved after logout button (far right)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: reorder desktop header - search in nav, logout on far right
- search trigger now in nav: @handle → search → settings
- logout button moved to far right (mirroring stats on left)
- hide portal link when already on /portal page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: swap search and portal link order in nav
nav order now: feed → liked → search → @handle → settings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* more stuff
* yuhhhhhhhh
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: collapse excess tags with +N button and fix mobile menu overlap
- limits visible tags to 2 by default (never wraps to second line)
- shows "+N" button when more tags exist
- clicking "+N" expands to show all tags (allows wrapping when expanded)
- uses flex-wrap: nowrap + overflow: hidden to guarantee single-line constraint
- collapses back when track changes (component recycle)
- fix LinksMenu and ProfileMenu mobile positioning to avoid player overlap
- menus now position from bottom with player height offset on mobile
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: keep mobile menus centered but shift up for player
- menus stay full-size and centered
- shift center point up by half player height when player is open
- cap max-height to avoid overlap with player
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
trigram similarity alone misses cases where a short query
(e.g. "real") is a substring of a word in a long title.
added OR condition with ILIKE to catch exact substring matches
while preserving fuzzy matching behavior for typos.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
replaced text characters (♪, ◉, ◫, #) with actual SVG icons
matching those used elsewhere in the app:
- track: music note icon (same as player placeholder)
- artist: person icon (same as TrackItem/TrackInfo)
- album: record icon (same as TrackItem/TrackInfo)
- tag: label icon (same as tag detail page)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- replaced hardcoded dark rgba colors with CSS variables
- added theme-aware scrollbar styling (scrollbar-color, webkit pseudo-elements)
- modal now respects light/dark/system theme setting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
changed conditional from `commentsEnabled !== false` to
`commentsEnabled === true` so the section only renders
after we explicitly know comments are enabled.
previously: commentsEnabled started as null, which passed
the !== false check, causing a brief flash before the API
response set it to false.
note: no regression test added - frontend lacks test infrastructure.
the fix is a one-line conditional change with clear before/after behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* docs: add unified search documentation
- update keyboard-shortcuts.md with Cmd/Ctrl+K search shortcut
- create comprehensive search.md covering frontend state, backend API,
database indexes (pg_trgm), fuzzy matching, and scaling considerations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add unified search to STATUS.md
- add recent work section for unified search (PR #447)
- move issue #440 from new features to working features
- add search docs to documentation links
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: unified search with Cmd/Ctrl+K shortcut
- add pg_trgm extension and GIN indexes for fuzzy search
- implement /search endpoint with trigram similarity scoring
- search across tracks, artists, albums, and tags
- create SearchModal component with keyboard navigation
- Cmd+K (Mac) / Ctrl+K (Windows/Linux) opens search
- arrow keys navigate, Enter selects, Esc closes
- results grouped by type with relevance scoring
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: improve search modal UX and styling
- use "Cmd+K" text instead of ⌘ symbol for clarity
- add client-side query length validation (max 100 chars)
- show clear error message when query exceeds limit
- apply glassmorphism styling to modal:
- frosted glass background with blur
- subtle border highlights
- refined hover/selected states
- consistent translucent elements
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: show artwork/avatars in search results
- display track artwork, artist avatars, album covers when available
- lazy loading with graceful fallback to icon on error
- image hidden on load failure, icon shown instead
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
STATUS.md:
- add theme system (PR #441): CSS custom properties, dark/light/system toggle
- add mobile UX improvements (PR #443): ProfileMenu, /upload page, portal overhaul
- add player scroll timing fix (PR #445): faster animations, "view track" link
- add CI optimization (PR #444): path-based hook skipping
- add issue #440 to new features list (unified search)
- update last modified date
docs/frontend/toast-notifications.md:
- document action links feature (was listed as "not implemented")
docs/frontend/state-management.md:
- update preferences to include theme and hidden tags
- mention ProfileMenu component for mobile
- fix upload flow to reference /upload page instead of portal
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Player scroll:
- remove the 1.5s hold at -100% (off-screen) position
- text now immediately resets after scrolling off
- reduce title animation from 10s to 8s
- reduce artist/album animation from 15s to 10s
Upload toast:
- fix duplicate "track uploaded successfully" toast
(was showing once on XHR complete, again on SSE complete)
- add "view track" link to success toast with track detail page
- toast action links now stay in same tab for internal URLs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
use dorny/paths-filter to detect what changed and only install
deps that are actually needed:
- frontend changes: install bun + frontend deps
- backend changes: install uv + backend deps
- other changes: no extra deps needed
this avoids installing all deps for every PR, reducing CI time
for frontend-only or backend-only changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Add ProfileMenu component for mobile (collapses profile/upload/settings/logout)
- Extract upload form to dedicated /upload page
- Standardize section headers across home and liked pages
- Comprehensive mobile styling for portal page:
- Tighten profile settings form
- Fix track edit/delete icon alignment with proper SVG icons
- Clean up data section typography
- Add track detail link under artwork
- Fix create token button alignment
- ProfileMenu hides contextual links (profile hidden on /portal, upload hidden on /upload)
- Upload card design in portal links to /upload page
- Fix layout inconsistencies between home and liked pages (padding, font-weight, mobile styles)
- Add globe icon in mobile header to navigate home when not on home page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- onboard: focus on STATUS.md as source of truth, remove verbose instructions
- status-update: align with actual STATUS.md structure, mention auto-archiving
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Replace hardcoded colors across 35 files with CSS custom properties
- Add theme switcher (dark/light/system) in settings menu
- Define semantic color tokens: bg-*, border-*, text-*, accent-*
- Light theme adapts all UI elements including tracks, header, tags
- Remove zen mode feature (was only hiding a few elements)
- Use color-mix() for derived colors to maintain consistency
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
when a track has the 'bufo' tag, semantically-matched toad GIFs
float across the track detail page. uses the track title as a
semantic search query against the find-bufo API.
- BufoEasterEgg component fetches toads based on track title
- results cached in localStorage for 1 week to reduce API calls
- TagEffects wrapper provides extensibility for future tag-based plugins
- animated toads drift across viewport with wobble effects
- respects prefers-reduced-motion
- fails gracefully if API is unavailable
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- track detail endpoint now fetches and returns tags
- album detail endpoint now fetches tags for all tracks
- track detail page displays tags with links to tag pages
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* chore: add ai tag to status maintenance uploads
uses plyrfm >= v0.0.1-alpha.10 which supports -t/--tag option
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update STATUS.md with tag filtering and SDK work
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
stopPropagation() on link click handlers breaks SvelteKit's client-side
router, causing full page reloads that unmount the player and interrupt
audio playback.
this pattern was previously fixed for artist links in commit a6376f6
but was reintroduced when tag links were added.
the parent button handler already checks if the click target is an anchor
element (lines 93-98), making stopPropagation() on the links unnecessary.
also adds docs/frontend/navigation.md documenting this pattern to prevent
future recurrence.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Move HiddenTagsFilter to be inline with "latest tracks" heading
instead of underneath it (cleaner on mobile)
- Create stats.svelte.ts cache to prevent stats from refetching
and flickering when navigating between home/liked pages
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Create preferences.svelte.ts store for centralized state management
- Load preferences in +layout.ts alongside auth check
- Update components to derive from preferences store instead of fetching independently:
- HiddenTagsFilter, SettingsMenu, ColorSettings, portal/+page
- Eliminates flash of incorrect state (e.g., eye icon) by ensuring preferences
are loaded before components render
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add tag detail pages and clickable tag links
- Add /tag/[name] route showing all tracks with a specific tag
- Make tag badges clickable links in TrackItem and portal
- Serve DEFAULT_HIDDEN_TAGS from /config endpoint for frontend
- Add tag validation utilities (TagValidationError, validate_tags_json)
- Add _get_or_create_tag helper with race condition handling
- Fix TrackResponse.from_track argument order bug in tags endpoint
- Fix HiddenTagsFilter UX duplication when adding tags
- Use CSS variables in TagInput for theme consistency
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: use pydantic TypeAdapter for tag validation
- Replace manual JSON parsing with TypeAdapter(list[str])
- Rename validate_tags_json -> parse_tags_json
- Remove custom TagValidationError, use ValueError
- Check pgcode 23505 specifically for unique constraint violations
in _get_or_create_tag (re-raise other IntegrityErrors)
🤖 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 tag filtering system with user-configurable hidden tags
- Add tags and track_tags tables with migrations
- Create /tracks/tags endpoint with autocomplete and track counts
- Add TagInput component for upload/edit forms with suggestions
- Implement hidden_tags preference (defaults to ["ai"])
- Filter hidden tags from latest tracks list
- Add hidden tags management UI in settings
- Fix route ordering so /tracks/tags works correctly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: move hidden tags filter inline with discovery feed
- Add explicit filter_hidden_tags parameter to /tracks endpoint
with smart defaults (auto-disable on artist pages)
- Move hidden tags UI from settings menu to inline eye icon
below "latest tracks" heading
- Collapsible filter: eye icon expands to show/edit hidden tags
- Add 5 regression tests for filter behavior
- Fix persistence: use $effect to fetch prefs when auth ready
🤖 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 with queue touch reorder and stats fix
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update cost structure to reflect current expenses
- fly.io: ~$10-15/month (production + staging backends + transcoder)
- neon: $5/month (upgraded to starter plan)
- audd: ~$10/month (enterprise API for copyright fingerprinting)
- total: ~$35-40/month (up from ~$5-6)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update README costs and clarify SDK/MCP link
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Add 6-dot grip handle on left side of each queue track for reordering
- Implement touch event handlers for mobile drag-and-drop
- Track visually follows finger during drag with translateY
- Drop target highlights while dragging over other tracks
- Always show drag handles and remove buttons on touch devices
- Desktop drag-and-drop still works by dragging anywhere on the track
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
The platform stats in the header were positioned using a static viewport
calculation that didn't account for the queue sidebar width. Now uses a
CSS custom property (--queue-width) that gets updated dynamically when
the queue visibility changes, ensuring the stats shift left along with
the rest of the header content.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* docs: note scale to zero disabled for cold start prevention
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* small prompt tweak
---------
Co-authored-by: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
after 5 minutes idle, Neon scales down and cold start takes 5-10s.
first requests after idle would exhaust the pool (5 connections),
causing all subsequent requests to fail with 500 errors.
changes:
- pool_size: 5 → 10 (more concurrent cold start requests)
- max_overflow: 0 → 5 (burst capacity to 15 connections)
- connection_timeout: 3s → 10s (wait for Neon wake-up)
this is a recurrence of the Nov 17 incident. that fix addressed the
queue listener's asyncpg connection but not the SQLAlchemy pool.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- update STATUS.md with accurate teal.fm integration context (speculative, awaiting feedback)
- rewrite workflow prompt to enforce 250 line limit
- add explicit first episode vs subsequent episode detection
- add detailed tone requirements with examples of what NOT to say
- add forbidden phrases list to reduce sycophancy
- instruct agent to fetch ATProto docs for accurate technical explanations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Was blocking all Bluesky avatars. Keep only explicit flagging.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: blur explicit images with user opt-in to show
- add explicit_images table to track flagged image URLs/IDs
- add show_explicit_artwork user preference (default: hidden)
- add /moderation/explicit-images endpoint to fetch flagged images
- add ExplicitImage component that blurs flagged images
- hide explicit images from og:image/twitter meta tags
- add portal toggle for explicit artwork preference
images can be flagged by image_id (R2) or full URL (external avatars).
frontend fetches the list once and checks all rendered images.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: use 'sensitive' instead of 'explicit' in tooltip
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: rename explicit to sensitive throughout codebase
- Rename table: explicit_images → sensitive_images
- Rename model: ExplicitImage → SensitiveImage
- Rename preference: show_explicit_artwork → show_sensitive_artwork
- Rename component: ExplicitImage.svelte → SensitiveImage.svelte
- Update all references, endpoints, and UI text
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
added note about intermittent audio playback hangs in iOS standalone
(PWA) mode. PR #466 added NetworkOnly caching for audio routes, but
iOS Safari is slow to update service workers. workaround is to delete
and re-add home screen bookmark. flagged for further investigation if
issue persists.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Space: play/pause
- Left/Right arrows: seek ±10 seconds
- J/L: previous/next track
- M: mute/unmute
All shortcuts respect input focus and are disabled when search modal is open.
Also fixes AGENTS.md to reflect that STATUS.md is now tracked.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
terminal-based vanity metrics dashboard using plotext for visualization.
features:
- zone stats: requests, pageviews, unique visitors, bandwidth, cache ratio
- daily requests bar chart (cyan)
- pageviews vs uniques line chart (green/magenta)
- CLI options: --days/-d for time window, --no-cache to force refresh
- automatic daily caching (~/.cache/plyr-analytics/)
- pydantic-settings for .env integration (CF_API_TOKEN, CF_ZONE_ID)
usage:
uv run scripts/cf_analytics.py # last 7 days
uv run scripts/cf_analytics.py -d 30 # last 30 days
uv run scripts/cf_analytics.py --no-cache
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the $effect that calls incrementPlayCount() runs on every currentTime
update (~4x/second). when threshold is first crossed, svelte's batched
reactive updates meant the guard (playCountedForTrack === currentTrack.id)
could miss rapid-fire calls, causing 2 scrobbles ~50-125ms apart.
fix: add synchronous (non-reactive) guard that blocks immediately,
before the async fetch fires.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add mutagen dependency for audio metadata extraction
- create backend/utilities/audio.py with extract_duration()
- extract duration during upload, store in track.extra["duration"]
- add backfill script for tracks uploaded before this feature
duration is now correctly passed to teal.fm scrobble records.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add Dec 4 section for PR #467 (teal.fm scrobbling)
- document lexicons, configuration, and code quality improvements
- update now-playing section to reflect native scrobbling
- add teal.fm to "what's working" list
- update last modified date
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* checkpoint
* checkpoint
* feat: teal.fm scrobbling integration
adds teal.fm scrobbling support - when users enable the toggle in settings
and re-authenticate, their plays are recorded to their PDS using teal's
ATProto lexicons.
changes:
- add TealSettings config class for configurable namespaces (TEAL_PLAY_COLLECTION,
TEAL_STATUS_COLLECTION, TEAL_ENABLED env vars)
- fix play count fetch missing credentials: 'include' (root cause of scrobbles
not triggering)
- add preferences.fetch() after login to ensure teal toggle state is current
- add unit tests for env var overrides (proves we can adapt when teal graduates
from alpha namespace)
the teal namespace defaults to fm.teal.alpha.* but can be changed via env vars
when teal.fm updates their lexicons.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: code quality improvements and bug fixes
- walrus operators throughout auth and playback code
- extract bearer token parsing into utilities/auth.py
- move DeveloperTokenInfo to top of file with field_validator for truncation
- fix now-playing firing every 1s (update lastReportedState in scheduled reports)
- use settings.frontend.url/domain for origin URLs (environment-aware)
- move teal.fm scrobbling setting from gear menu to portal "Your Data" section
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
audio streaming was intermittently hanging in iOS standalone (PWA) mode.
the service worker was intercepting /audio/{file_id} requests with
NetworkFirst caching, which caused issues with:
- 307 redirects to R2 CDN getting cached/stale
- range request headers not being handled properly on iOS Safari
switched to NetworkOnly for audio routes so the SW passes through
without interference. we weren't actually caching audio files anyway
(just the redirect response), so there's no functional loss.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
The search modal was using `visibility: hidden` to hide the backdrop,
which prevented the input from receiving focus. This caused:
- No text cursor on desktop when pressing Cmd+K
- No keyboard popup on mobile when tapping search icon
Changed to use only `opacity: 0` (already present) for hiding.
Elements with `opacity: 0` remain focusable, allowing the synchronous
focus() call in search.open() to work correctly - critical for mobile
keyboards which only open when focus is in the same call stack as the
user gesture.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the now-playing endpoints were hitting the global 100/minute rate limit
when multiple users/tabs were active simultaneously. since these endpoints
are already throttled client-side (10-second intervals, 1-second debounce,
5-second progress buckets), server-side rate limiting is unnecessary.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the documented commands didn't match actual justfile recipes:
- `just frontend install` -> doesn't exist
- `just run-backend` -> `just backend run`
- `just lint` -> `just backend lint` / `just frontend check`
- `just migrate` -> `just backend migrate`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
when a track doesn't have its own artwork but belongs to an album
with artwork, use the album artwork instead of the placeholder icon.
applies to:
- player artwork thumbnail (TrackInfo.svelte)
- media session metadata for system controls (Player.svelte)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
iOS/mobile browsers only open keyboard when focus() is called directly
in a user gesture handler. Previously, focus happened in a Svelte $effect
after state change, which broke the gesture chain.
Fix: Always render SearchModal (hidden via CSS), register input ref with
search state, and focus directly in search.open() before state change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: move search to top-level mobile header
- Add search icon button to mobile-center nav (left of feed/liked icons)
- Remove search from ProfileMenu dropdown
- Space icons evenly across mobile header
- Cleaner mobile UX with one less tap to search
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: hide keyboard hints on mobile, smaller placeholder text
- Hide Cmd+K shortcut badge on mobile
- Hide keyboard navigation hints (arrows, esc) on mobile
- Shrink placeholder text on mobile to prevent overflow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: collapse excess tags with +N button and fix mobile menu overlap
- limits visible tags to 2 by default (never wraps to second line)
- shows "+N" button when more tags exist
- clicking "+N" expands to show all tags (allows wrapping when expanded)
- uses flex-wrap: nowrap + overflow: hidden to guarantee single-line constraint
- collapses back when track changes (component recycle)
- fix LinksMenu and ProfileMenu mobile positioning to avoid player overlap
- menus now position from bottom with player height offset on mobile
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: keep mobile menus centered but shift up for player
- menus stay full-size and centered
- shift center point up by half player height when player is open
- cap max-height to avoid overlap with player
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add search trigger to header and mobile menu
desktop:
- SearchTrigger component shows magnifying glass + keyboard shortcut hint
- detects platform for correct shortcut (⌘K on Mac, Ctrl+K on Windows/Linux)
- subtle styling with accent highlight on hover
mobile:
- search added as first item in ProfileMenu (three-dot menu)
- styled with accent tint background to stand out
- opens search modal and closes menu
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: hide portal link on portal page, move search to far right
- portal link (@handle) now hidden when already on /portal
- search trigger moved after logout button (far right)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: reorder desktop header - search in nav, logout on far right
- search trigger now in nav: @handle → search → settings
- logout button moved to far right (mirroring stats on left)
- hide portal link when already on /portal page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: swap search and portal link order in nav
nav order now: feed → liked → search → @handle → settings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* more stuff
* yuhhhhhhhh
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: collapse excess tags with +N button and fix mobile menu overlap
- limits visible tags to 2 by default (never wraps to second line)
- shows "+N" button when more tags exist
- clicking "+N" expands to show all tags (allows wrapping when expanded)
- uses flex-wrap: nowrap + overflow: hidden to guarantee single-line constraint
- collapses back when track changes (component recycle)
- fix LinksMenu and ProfileMenu mobile positioning to avoid player overlap
- menus now position from bottom with player height offset on mobile
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: keep mobile menus centered but shift up for player
- menus stay full-size and centered
- shift center point up by half player height when player is open
- cap max-height to avoid overlap with player
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
trigram similarity alone misses cases where a short query
(e.g. "real") is a substring of a word in a long title.
added OR condition with ILIKE to catch exact substring matches
while preserving fuzzy matching behavior for typos.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
replaced text characters (♪, ◉, ◫, #) with actual SVG icons
matching those used elsewhere in the app:
- track: music note icon (same as player placeholder)
- artist: person icon (same as TrackItem/TrackInfo)
- album: record icon (same as TrackItem/TrackInfo)
- tag: label icon (same as tag detail page)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
changed conditional from `commentsEnabled !== false` to
`commentsEnabled === true` so the section only renders
after we explicitly know comments are enabled.
previously: commentsEnabled started as null, which passed
the !== false check, causing a brief flash before the API
response set it to false.
note: no regression test added - frontend lacks test infrastructure.
the fix is a one-line conditional change with clear before/after behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* docs: add unified search documentation
- update keyboard-shortcuts.md with Cmd/Ctrl+K search shortcut
- create comprehensive search.md covering frontend state, backend API,
database indexes (pg_trgm), fuzzy matching, and scaling considerations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add unified search to STATUS.md
- add recent work section for unified search (PR #447)
- move issue #440 from new features to working features
- add search docs to documentation links
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: unified search with Cmd/Ctrl+K shortcut
- add pg_trgm extension and GIN indexes for fuzzy search
- implement /search endpoint with trigram similarity scoring
- search across tracks, artists, albums, and tags
- create SearchModal component with keyboard navigation
- Cmd+K (Mac) / Ctrl+K (Windows/Linux) opens search
- arrow keys navigate, Enter selects, Esc closes
- results grouped by type with relevance scoring
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: improve search modal UX and styling
- use "Cmd+K" text instead of ⌘ symbol for clarity
- add client-side query length validation (max 100 chars)
- show clear error message when query exceeds limit
- apply glassmorphism styling to modal:
- frosted glass background with blur
- subtle border highlights
- refined hover/selected states
- consistent translucent elements
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: show artwork/avatars in search results
- display track artwork, artist avatars, album covers when available
- lazy loading with graceful fallback to icon on error
- image hidden on load failure, icon shown instead
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
STATUS.md:
- add theme system (PR #441): CSS custom properties, dark/light/system toggle
- add mobile UX improvements (PR #443): ProfileMenu, /upload page, portal overhaul
- add player scroll timing fix (PR #445): faster animations, "view track" link
- add CI optimization (PR #444): path-based hook skipping
- add issue #440 to new features list (unified search)
- update last modified date
docs/frontend/toast-notifications.md:
- document action links feature (was listed as "not implemented")
docs/frontend/state-management.md:
- update preferences to include theme and hidden tags
- mention ProfileMenu component for mobile
- fix upload flow to reference /upload page instead of portal
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Player scroll:
- remove the 1.5s hold at -100% (off-screen) position
- text now immediately resets after scrolling off
- reduce title animation from 10s to 8s
- reduce artist/album animation from 15s to 10s
Upload toast:
- fix duplicate "track uploaded successfully" toast
(was showing once on XHR complete, again on SSE complete)
- add "view track" link to success toast with track detail page
- toast action links now stay in same tab for internal URLs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
use dorny/paths-filter to detect what changed and only install
deps that are actually needed:
- frontend changes: install bun + frontend deps
- backend changes: install uv + backend deps
- other changes: no extra deps needed
this avoids installing all deps for every PR, reducing CI time
for frontend-only or backend-only changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Add ProfileMenu component for mobile (collapses profile/upload/settings/logout)
- Extract upload form to dedicated /upload page
- Standardize section headers across home and liked pages
- Comprehensive mobile styling for portal page:
- Tighten profile settings form
- Fix track edit/delete icon alignment with proper SVG icons
- Clean up data section typography
- Add track detail link under artwork
- Fix create token button alignment
- ProfileMenu hides contextual links (profile hidden on /portal, upload hidden on /upload)
- Upload card design in portal links to /upload page
- Fix layout inconsistencies between home and liked pages (padding, font-weight, mobile styles)
- Add globe icon in mobile header to navigate home when not on home page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Replace hardcoded colors across 35 files with CSS custom properties
- Add theme switcher (dark/light/system) in settings menu
- Define semantic color tokens: bg-*, border-*, text-*, accent-*
- Light theme adapts all UI elements including tracks, header, tags
- Remove zen mode feature (was only hiding a few elements)
- Use color-mix() for derived colors to maintain consistency
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
when a track has the 'bufo' tag, semantically-matched toad GIFs
float across the track detail page. uses the track title as a
semantic search query against the find-bufo API.
- BufoEasterEgg component fetches toads based on track title
- results cached in localStorage for 1 week to reduce API calls
- TagEffects wrapper provides extensibility for future tag-based plugins
- animated toads drift across viewport with wobble effects
- respects prefers-reduced-motion
- fails gracefully if API is unavailable
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* chore: add ai tag to status maintenance uploads
uses plyrfm >= v0.0.1-alpha.10 which supports -t/--tag option
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update STATUS.md with tag filtering and SDK work
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
stopPropagation() on link click handlers breaks SvelteKit's client-side
router, causing full page reloads that unmount the player and interrupt
audio playback.
this pattern was previously fixed for artist links in commit a6376f6
but was reintroduced when tag links were added.
the parent button handler already checks if the click target is an anchor
element (lines 93-98), making stopPropagation() on the links unnecessary.
also adds docs/frontend/navigation.md documenting this pattern to prevent
future recurrence.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Move HiddenTagsFilter to be inline with "latest tracks" heading
instead of underneath it (cleaner on mobile)
- Create stats.svelte.ts cache to prevent stats from refetching
and flickering when navigating between home/liked pages
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Create preferences.svelte.ts store for centralized state management
- Load preferences in +layout.ts alongside auth check
- Update components to derive from preferences store instead of fetching independently:
- HiddenTagsFilter, SettingsMenu, ColorSettings, portal/+page
- Eliminates flash of incorrect state (e.g., eye icon) by ensuring preferences
are loaded before components render
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add tag detail pages and clickable tag links
- Add /tag/[name] route showing all tracks with a specific tag
- Make tag badges clickable links in TrackItem and portal
- Serve DEFAULT_HIDDEN_TAGS from /config endpoint for frontend
- Add tag validation utilities (TagValidationError, validate_tags_json)
- Add _get_or_create_tag helper with race condition handling
- Fix TrackResponse.from_track argument order bug in tags endpoint
- Fix HiddenTagsFilter UX duplication when adding tags
- Use CSS variables in TagInput for theme consistency
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: use pydantic TypeAdapter for tag validation
- Replace manual JSON parsing with TypeAdapter(list[str])
- Rename validate_tags_json -> parse_tags_json
- Remove custom TagValidationError, use ValueError
- Check pgcode 23505 specifically for unique constraint violations
in _get_or_create_tag (re-raise other IntegrityErrors)
🤖 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 tag filtering system with user-configurable hidden tags
- Add tags and track_tags tables with migrations
- Create /tracks/tags endpoint with autocomplete and track counts
- Add TagInput component for upload/edit forms with suggestions
- Implement hidden_tags preference (defaults to ["ai"])
- Filter hidden tags from latest tracks list
- Add hidden tags management UI in settings
- Fix route ordering so /tracks/tags works correctly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: move hidden tags filter inline with discovery feed
- Add explicit filter_hidden_tags parameter to /tracks endpoint
with smart defaults (auto-disable on artist pages)
- Move hidden tags UI from settings menu to inline eye icon
below "latest tracks" heading
- Collapsible filter: eye icon expands to show/edit hidden tags
- Add 5 regression tests for filter behavior
- Fix persistence: use $effect to fetch prefs when auth ready
🤖 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 with queue touch reorder and stats fix
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update cost structure to reflect current expenses
- fly.io: ~$10-15/month (production + staging backends + transcoder)
- neon: $5/month (upgraded to starter plan)
- audd: ~$10/month (enterprise API for copyright fingerprinting)
- total: ~$35-40/month (up from ~$5-6)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update README costs and clarify SDK/MCP link
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Add 6-dot grip handle on left side of each queue track for reordering
- Implement touch event handlers for mobile drag-and-drop
- Track visually follows finger during drag with translateY
- Drop target highlights while dragging over other tracks
- Always show drag handles and remove buttons on touch devices
- Desktop drag-and-drop still works by dragging anywhere on the track
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
The platform stats in the header were positioned using a static viewport
calculation that didn't account for the queue sidebar width. Now uses a
CSS custom property (--queue-width) that gets updated dynamically when
the queue visibility changes, ensuring the stats shift left along with
the rest of the header content.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
after 5 minutes idle, Neon scales down and cold start takes 5-10s.
first requests after idle would exhaust the pool (5 connections),
causing all subsequent requests to fail with 500 errors.
changes:
- pool_size: 5 → 10 (more concurrent cold start requests)
- max_overflow: 0 → 5 (burst capacity to 15 connections)
- connection_timeout: 3s → 10s (wait for Neon wake-up)
this is a recurrence of the Nov 17 incident. that fix addressed the
queue listener's asyncpg connection but not the SQLAlchemy pool.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- update STATUS.md with accurate teal.fm integration context (speculative, awaiting feedback)
- rewrite workflow prompt to enforce 250 line limit
- add explicit first episode vs subsequent episode detection
- add detailed tone requirements with examples of what NOT to say
- add forbidden phrases list to reduce sycophancy
- instruct agent to fetch ATProto docs for accurate technical explanations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>