commits
cache key was just the query, so pattern config changes didn't
invalidate cached results. now includes exclude/include patterns
so config changes take effect immediately.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
adds BUFO_EXCLUDE_PATTERNS and BUFO_INCLUDE_PATTERNS env vars to control
which bufo images appear in the easter egg animation.
- exclude: regex patterns to filter out (default: ^bigbufo_)
- include: allowlist that overrides exclude (default: bigbufo_0_0, bigbufo_2_1)
adds CommaSeparatedStringSet type for parsing comma-delimited env vars.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- sensitive image coverage: complete coverage across all image displays
including media session, embeds, search results, album pages
- mobile artwork upload: iOS HEIC handling via content_type detection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
on iOS, selecting a HEIC photo from the Photos library often results
in a file with a .heic filename but jpeg content (iOS converts on the
fly). the backend was using only the filename extension to validate
image format, causing these files to be silently rejected.
changes:
- backend: add from_content_type() method to ImageFormat
- backend: validate_and_extract() now prefers content_type over extension
- backend: pass image content_type through upload pipeline
- frontend: use accept="image/*" for broader iOS compatibility
- tests: add coverage for iOS HEIC case
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: respect sensitive artwork preference in media session
the media session API (CarPlay, lock screen, control center) was
showing unblurred artwork for tracks flagged as sensitive, even when
the user had `show_sensitive_artwork` disabled.
now checks each artwork candidate against the moderation list and
the user's preference before including it in media session metadata.
if all artwork is sensitive and the user prefers not to see it, the
media session will display no artwork.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: apply SensitiveImage wrapper to remaining image displays
extends sensitive content handling to:
- embed page (track artwork)
- album detail page (artwork + og:image meta tags)
- artist page album grid
- track page comment avatars
- search modal results
- handle search/autocomplete avatars
uses compact mode for small avatars (blur only, no tooltip).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Fix local dev section: uses Neon dev database, not localhost
- Correct Fly.io app name from plyr-api to relay-api throughout
- Add "current capabilities" section documenting that migration
isolation (via release_command) and multi-environment pipeline
(dev/staging/prod) are already implemented
- Update database diagram to show all four databases (dev, staging,
prod on Neon + local test)
- Remove misleading "future considerations" for features that exist
Co-authored-by: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the previous fix only elevated the .likes element, but z-index on a child
doesn't help when sibling track-containers are in the same stacking context.
now:
- .track-container.likers-tooltip-open gets z-index: 60 (above header at 50)
- removes the ineffective .likes.tooltip-open z-index
- the entire track elevates above siblings, ensuring the tooltip renders on top
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
z-index fix:
- remove static z-index from .likes element
- only apply z-index: 10 when tooltip is open (.likes.tooltip-open)
- this ensures the active tooltip's parent elevates above sibling tracks
without all tracks competing at the same z-index level
flip detection:
- detect when likes element is <300px from viewport top
- flip tooltip to appear below instead of above
- prevents tooltip from colliding with sticky header
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* docs: add sensitive image moderation to STATUS.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add sensitive content moderation documentation
documents the sensitive image moderation system:
- database schema (sensitive_images table)
- frontend architecture (SSR + client-side)
- SensitiveImage component usage
- matching logic for R2 and external URLs
- API endpoint
- user experience flow
- current limitations (manual flagging only)
- future improvements needed (perceptual hashing, AI detection)
- moderation workflow with SQL examples
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: likers tooltip z-index and compact mode for sensitive avatars
- add z-index to .likes element so tooltip renders above header border
- add compact prop to SensitiveImage for avatars in lists (blur only, no tooltip, preserves layout)
- use compact mode in LikersTooltip to prevent layout breakage on blurred avatars
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Wrap liker avatars in SensitiveImage component
- Add max-height (240px) and scrolling to likers list
- Fix hover interaction: delay close to allow entering tooltip
- Add Escape key to close tooltip
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: use data prop instead of parent() for sensitive images SSR
The +layout.ts file was incorrectly using parent() to access data from
+layout.server.ts. In SvelteKit, data from a server load function in the
same route comes via the data parameter, not parent(). parent() is only
for accessing data from parent routes.
This fixes SSR meta tag generation for sensitive images.
* fix: handle nullable data parameter in layout load function
TypeScript was complaining that data could be null. Use optional
chaining and extract sensitiveImages to a variable to avoid repetition.
Link previews weren't filtering sensitive images because the moderation
data was only fetched client-side. Now we fetch it in +layout.server.ts
and pass it through to pages for SSR-safe meta tag filtering.
- Add +layout.server.ts to fetch sensitive images
- Export checkImageSensitive() utility for shared use
- Update track and artist pages to use SSR-safe checks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Was blocking all Bluesky avatars. Keep only explicit flagging.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Fix regex to match R2 URLs (pub-*.r2.dev/{id}.{ext})
- Blur non-R2 images by default as they could be injected via ATProto records
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: blur explicit images with user opt-in to show
- add explicit_images table to track flagged image URLs/IDs
- add show_explicit_artwork user preference (default: hidden)
- add /moderation/explicit-images endpoint to fetch flagged images
- add ExplicitImage component that blurs flagged images
- hide explicit images from og:image/twitter meta tags
- add portal toggle for explicit artwork preference
images can be flagged by image_id (R2) or full URL (external avatars).
frontend fetches the list once and checks all rendered images.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: use 'sensitive' instead of 'explicit' in tooltip
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: rename explicit to sensitive throughout codebase
- Rename table: explicit_images → sensitive_images
- Rename model: ExplicitImage → SensitiveImage
- Rename preference: show_explicit_artwork → show_sensitive_artwork
- Rename component: ExplicitImage.svelte → SensitiveImage.svelte
- Update all references, endpoints, and UI text
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
added note about intermittent audio playback hangs in iOS standalone
(PWA) mode. PR #466 added NetworkOnly caching for audio routes, but
iOS Safari is slow to update service workers. workaround is to delete
and re-add home screen bookmark. flagged for further investigation if
issue persists.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Space: play/pause
- Left/Right arrows: seek ±10 seconds
- J/L: previous/next track
- M: mute/unmute
All shortcuts respect input focus and are disabled when search modal is open.
Also fixes AGENTS.md to reflect that STATUS.md is now tracked.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
terminal-based vanity metrics dashboard using plotext for visualization.
features:
- zone stats: requests, pageviews, unique visitors, bandwidth, cache ratio
- daily requests bar chart (cyan)
- pageviews vs uniques line chart (green/magenta)
- CLI options: --days/-d for time window, --no-cache to force refresh
- automatic daily caching (~/.cache/plyr-analytics/)
- pydantic-settings for .env integration (CF_API_TOKEN, CF_ZONE_ID)
usage:
uv run scripts/cf_analytics.py # last 7 days
uv run scripts/cf_analytics.py -d 30 # last 30 days
uv run scripts/cf_analytics.py --no-cache
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the $effect that calls incrementPlayCount() runs on every currentTime
update (~4x/second). when threshold is first crossed, svelte's batched
reactive updates meant the guard (playCountedForTrack === currentTrack.id)
could miss rapid-fire calls, causing 2 scrobbles ~50-125ms apart.
fix: add synchronous (non-reactive) guard that blocks immediately,
before the async fetch fires.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add mutagen dependency for audio metadata extraction
- create backend/utilities/audio.py with extract_duration()
- extract duration during upload, store in track.extra["duration"]
- add backfill script for tracks uploaded before this feature
duration is now correctly passed to teal.fm scrobble records.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add Dec 4 section for PR #467 (teal.fm scrobbling)
- document lexicons, configuration, and code quality improvements
- update now-playing section to reflect native scrobbling
- add teal.fm to "what's working" list
- update last modified date
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* checkpoint
* checkpoint
* feat: teal.fm scrobbling integration
adds teal.fm scrobbling support - when users enable the toggle in settings
and re-authenticate, their plays are recorded to their PDS using teal's
ATProto lexicons.
changes:
- add TealSettings config class for configurable namespaces (TEAL_PLAY_COLLECTION,
TEAL_STATUS_COLLECTION, TEAL_ENABLED env vars)
- fix play count fetch missing credentials: 'include' (root cause of scrobbles
not triggering)
- add preferences.fetch() after login to ensure teal toggle state is current
- add unit tests for env var overrides (proves we can adapt when teal graduates
from alpha namespace)
the teal namespace defaults to fm.teal.alpha.* but can be changed via env vars
when teal.fm updates their lexicons.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: code quality improvements and bug fixes
- walrus operators throughout auth and playback code
- extract bearer token parsing into utilities/auth.py
- move DeveloperTokenInfo to top of file with field_validator for truncation
- fix now-playing firing every 1s (update lastReportedState in scheduled reports)
- use settings.frontend.url/domain for origin URLs (environment-aware)
- move teal.fm scrobbling setting from gear menu to portal "Your Data" section
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
audio streaming was intermittently hanging in iOS standalone (PWA) mode.
the service worker was intercepting /audio/{file_id} requests with
NetworkFirst caching, which caused issues with:
- 307 redirects to R2 CDN getting cached/stale
- range request headers not being handled properly on iOS Safari
switched to NetworkOnly for audio routes so the SW passes through
without interference. we weren't actually caching audio files anyway
(just the redirect response), so there's no functional loss.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
The logout button was rendered outside the isAuthenticated check,
causing both login and logout buttons to appear on desktop when
logged out.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
The search modal was using `visibility: hidden` to hide the backdrop,
which prevented the input from receiving focus. This caused:
- No text cursor on desktop when pressing Cmd+K
- No keyboard popup on mobile when tapping search icon
Changed to use only `opacity: 0` (already present) for hiding.
Elements with `opacity: 0` remain focusable, allowing the synchronous
focus() call in search.open() to work correctly - critical for mobile
keyboards which only open when focus is in the same call stack as the
user gesture.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Removes debug logging that was left in from initial development in PR #233.
Keeps console.error for actual error handling.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
uv run by default syncs all dependencies including dev deps, which
caused the release command to hang downloading ruff, jedi, etc.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the now-playing endpoints were hitting the global 100/minute rate limit
when multiple users/tabs were active simultaneously. since these endpoints
are already throttled client-side (10-second intervals, 1-second debounce,
5-second progress buckets), server-side rate limiting is unnecessary.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the documented commands didn't match actual justfile recipes:
- `just frontend install` -> doesn't exist
- `just run-backend` -> `just backend run`
- `just lint` -> `just backend lint` / `just frontend check`
- `just migrate` -> `just backend migrate`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
when a track doesn't have its own artwork but belongs to an album
with artwork, use the album artwork instead of the placeholder icon.
applies to:
- player artwork thumbnail (TrackInfo.svelte)
- media session metadata for system controls (Player.svelte)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
iOS/mobile browsers only open keyboard when focus() is called directly
in a user gesture handler. Previously, focus happened in a Svelte $effect
after state change, which broke the gesture chain.
Fix: Always render SearchModal (hidden via CSS), register input ref with
search state, and focus directly in search.open() before state change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: move search to top-level mobile header
- Add search icon button to mobile-center nav (left of feed/liked icons)
- Remove search from ProfileMenu dropdown
- Space icons evenly across mobile header
- Cleaner mobile UX with one less tap to search
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: hide keyboard hints on mobile, smaller placeholder text
- Hide Cmd+K shortcut badge on mobile
- Hide keyboard navigation hints (arrows, esc) on mobile
- Shrink placeholder text on mobile to prevent overflow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: collapse excess tags with +N button and fix mobile menu overlap
- limits visible tags to 2 by default (never wraps to second line)
- shows "+N" button when more tags exist
- clicking "+N" expands to show all tags (allows wrapping when expanded)
- uses flex-wrap: nowrap + overflow: hidden to guarantee single-line constraint
- collapses back when track changes (component recycle)
- fix LinksMenu and ProfileMenu mobile positioning to avoid player overlap
- menus now position from bottom with player height offset on mobile
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: keep mobile menus centered but shift up for player
- menus stay full-size and centered
- shift center point up by half player height when player is open
- cap max-height to avoid overlap with player
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add search trigger to header and mobile menu
desktop:
- SearchTrigger component shows magnifying glass + keyboard shortcut hint
- detects platform for correct shortcut (⌘K on Mac, Ctrl+K on Windows/Linux)
- subtle styling with accent highlight on hover
mobile:
- search added as first item in ProfileMenu (three-dot menu)
- styled with accent tint background to stand out
- opens search modal and closes menu
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: hide portal link on portal page, move search to far right
- portal link (@handle) now hidden when already on /portal
- search trigger moved after logout button (far right)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: reorder desktop header - search in nav, logout on far right
- search trigger now in nav: @handle → search → settings
- logout button moved to far right (mirroring stats on left)
- hide portal link when already on /portal page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: swap search and portal link order in nav
nav order now: feed → liked → search → @handle → settings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* more stuff
* yuhhhhhhhh
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: collapse excess tags with +N button and fix mobile menu overlap
- limits visible tags to 2 by default (never wraps to second line)
- shows "+N" button when more tags exist
- clicking "+N" expands to show all tags (allows wrapping when expanded)
- uses flex-wrap: nowrap + overflow: hidden to guarantee single-line constraint
- collapses back when track changes (component recycle)
- fix LinksMenu and ProfileMenu mobile positioning to avoid player overlap
- menus now position from bottom with player height offset on mobile
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: keep mobile menus centered but shift up for player
- menus stay full-size and centered
- shift center point up by half player height when player is open
- cap max-height to avoid overlap with player
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
trigram similarity alone misses cases where a short query
(e.g. "real") is a substring of a word in a long title.
added OR condition with ILIKE to catch exact substring matches
while preserving fuzzy matching behavior for typos.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
replaced text characters (♪, ◉, ◫, #) with actual SVG icons
matching those used elsewhere in the app:
- track: music note icon (same as player placeholder)
- artist: person icon (same as TrackItem/TrackInfo)
- album: record icon (same as TrackItem/TrackInfo)
- tag: label icon (same as tag detail page)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- replaced hardcoded dark rgba colors with CSS variables
- added theme-aware scrollbar styling (scrollbar-color, webkit pseudo-elements)
- modal now respects light/dark/system theme setting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
changed conditional from `commentsEnabled !== false` to
`commentsEnabled === true` so the section only renders
after we explicitly know comments are enabled.
previously: commentsEnabled started as null, which passed
the !== false check, causing a brief flash before the API
response set it to false.
note: no regression test added - frontend lacks test infrastructure.
the fix is a one-line conditional change with clear before/after behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* docs: add unified search documentation
- update keyboard-shortcuts.md with Cmd/Ctrl+K search shortcut
- create comprehensive search.md covering frontend state, backend API,
database indexes (pg_trgm), fuzzy matching, and scaling considerations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add unified search to STATUS.md
- add recent work section for unified search (PR #447)
- move issue #440 from new features to working features
- add search docs to documentation links
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: unified search with Cmd/Ctrl+K shortcut
- add pg_trgm extension and GIN indexes for fuzzy search
- implement /search endpoint with trigram similarity scoring
- search across tracks, artists, albums, and tags
- create SearchModal component with keyboard navigation
- Cmd+K (Mac) / Ctrl+K (Windows/Linux) opens search
- arrow keys navigate, Enter selects, Esc closes
- results grouped by type with relevance scoring
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: improve search modal UX and styling
- use "Cmd+K" text instead of ⌘ symbol for clarity
- add client-side query length validation (max 100 chars)
- show clear error message when query exceeds limit
- apply glassmorphism styling to modal:
- frosted glass background with blur
- subtle border highlights
- refined hover/selected states
- consistent translucent elements
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: show artwork/avatars in search results
- display track artwork, artist avatars, album covers when available
- lazy loading with graceful fallback to icon on error
- image hidden on load failure, icon shown instead
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
STATUS.md:
- add theme system (PR #441): CSS custom properties, dark/light/system toggle
- add mobile UX improvements (PR #443): ProfileMenu, /upload page, portal overhaul
- add player scroll timing fix (PR #445): faster animations, "view track" link
- add CI optimization (PR #444): path-based hook skipping
- add issue #440 to new features list (unified search)
- update last modified date
docs/frontend/toast-notifications.md:
- document action links feature (was listed as "not implemented")
docs/frontend/state-management.md:
- update preferences to include theme and hidden tags
- mention ProfileMenu component for mobile
- fix upload flow to reference /upload page instead of portal
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Player scroll:
- remove the 1.5s hold at -100% (off-screen) position
- text now immediately resets after scrolling off
- reduce title animation from 10s to 8s
- reduce artist/album animation from 15s to 10s
Upload toast:
- fix duplicate "track uploaded successfully" toast
(was showing once on XHR complete, again on SSE complete)
- add "view track" link to success toast with track detail page
- toast action links now stay in same tab for internal URLs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
use dorny/paths-filter to detect what changed and only install
deps that are actually needed:
- frontend changes: install bun + frontend deps
- backend changes: install uv + backend deps
- other changes: no extra deps needed
this avoids installing all deps for every PR, reducing CI time
for frontend-only or backend-only changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Add ProfileMenu component for mobile (collapses profile/upload/settings/logout)
- Extract upload form to dedicated /upload page
- Standardize section headers across home and liked pages
- Comprehensive mobile styling for portal page:
- Tighten profile settings form
- Fix track edit/delete icon alignment with proper SVG icons
- Clean up data section typography
- Add track detail link under artwork
- Fix create token button alignment
- ProfileMenu hides contextual links (profile hidden on /portal, upload hidden on /upload)
- Upload card design in portal links to /upload page
- Fix layout inconsistencies between home and liked pages (padding, font-weight, mobile styles)
- Add globe icon in mobile header to navigate home when not on home page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- onboard: focus on STATUS.md as source of truth, remove verbose instructions
- status-update: align with actual STATUS.md structure, mention auto-archiving
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Replace hardcoded colors across 35 files with CSS custom properties
- Add theme switcher (dark/light/system) in settings menu
- Define semantic color tokens: bg-*, border-*, text-*, accent-*
- Light theme adapts all UI elements including tracks, header, tags
- Remove zen mode feature (was only hiding a few elements)
- Use color-mix() for derived colors to maintain consistency
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
when a track has the 'bufo' tag, semantically-matched toad GIFs
float across the track detail page. uses the track title as a
semantic search query against the find-bufo API.
- BufoEasterEgg component fetches toads based on track title
- results cached in localStorage for 1 week to reduce API calls
- TagEffects wrapper provides extensibility for future tag-based plugins
- animated toads drift across viewport with wobble effects
- respects prefers-reduced-motion
- fails gracefully if API is unavailable
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- track detail endpoint now fetches and returns tags
- album detail endpoint now fetches tags for all tracks
- track detail page displays tags with links to tag pages
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* chore: add ai tag to status maintenance uploads
uses plyrfm >= v0.0.1-alpha.10 which supports -t/--tag option
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update STATUS.md with tag filtering and SDK work
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
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>
cache key was just the query, so pattern config changes didn't
invalidate cached results. now includes exclude/include patterns
so config changes take effect immediately.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
adds BUFO_EXCLUDE_PATTERNS and BUFO_INCLUDE_PATTERNS env vars to control
which bufo images appear in the easter egg animation.
- exclude: regex patterns to filter out (default: ^bigbufo_)
- include: allowlist that overrides exclude (default: bigbufo_0_0, bigbufo_2_1)
adds CommaSeparatedStringSet type for parsing comma-delimited env vars.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- sensitive image coverage: complete coverage across all image displays
including media session, embeds, search results, album pages
- mobile artwork upload: iOS HEIC handling via content_type detection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
on iOS, selecting a HEIC photo from the Photos library often results
in a file with a .heic filename but jpeg content (iOS converts on the
fly). the backend was using only the filename extension to validate
image format, causing these files to be silently rejected.
changes:
- backend: add from_content_type() method to ImageFormat
- backend: validate_and_extract() now prefers content_type over extension
- backend: pass image content_type through upload pipeline
- frontend: use accept="image/*" for broader iOS compatibility
- tests: add coverage for iOS HEIC case
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: respect sensitive artwork preference in media session
the media session API (CarPlay, lock screen, control center) was
showing unblurred artwork for tracks flagged as sensitive, even when
the user had `show_sensitive_artwork` disabled.
now checks each artwork candidate against the moderation list and
the user's preference before including it in media session metadata.
if all artwork is sensitive and the user prefers not to see it, the
media session will display no artwork.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: apply SensitiveImage wrapper to remaining image displays
extends sensitive content handling to:
- embed page (track artwork)
- album detail page (artwork + og:image meta tags)
- artist page album grid
- track page comment avatars
- search modal results
- handle search/autocomplete avatars
uses compact mode for small avatars (blur only, no tooltip).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Fix local dev section: uses Neon dev database, not localhost
- Correct Fly.io app name from plyr-api to relay-api throughout
- Add "current capabilities" section documenting that migration
isolation (via release_command) and multi-environment pipeline
(dev/staging/prod) are already implemented
- Update database diagram to show all four databases (dev, staging,
prod on Neon + local test)
- Remove misleading "future considerations" for features that exist
Co-authored-by: Claude <noreply@anthropic.com>
the previous fix only elevated the .likes element, but z-index on a child
doesn't help when sibling track-containers are in the same stacking context.
now:
- .track-container.likers-tooltip-open gets z-index: 60 (above header at 50)
- removes the ineffective .likes.tooltip-open z-index
- the entire track elevates above siblings, ensuring the tooltip renders on top
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
z-index fix:
- remove static z-index from .likes element
- only apply z-index: 10 when tooltip is open (.likes.tooltip-open)
- this ensures the active tooltip's parent elevates above sibling tracks
without all tracks competing at the same z-index level
flip detection:
- detect when likes element is <300px from viewport top
- flip tooltip to appear below instead of above
- prevents tooltip from colliding with sticky header
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* docs: add sensitive image moderation to STATUS.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add sensitive content moderation documentation
documents the sensitive image moderation system:
- database schema (sensitive_images table)
- frontend architecture (SSR + client-side)
- SensitiveImage component usage
- matching logic for R2 and external URLs
- API endpoint
- user experience flow
- current limitations (manual flagging only)
- future improvements needed (perceptual hashing, AI detection)
- moderation workflow with SQL examples
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: likers tooltip z-index and compact mode for sensitive avatars
- add z-index to .likes element so tooltip renders above header border
- add compact prop to SensitiveImage for avatars in lists (blur only, no tooltip, preserves layout)
- use compact mode in LikersTooltip to prevent layout breakage on blurred avatars
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Wrap liker avatars in SensitiveImage component
- Add max-height (240px) and scrolling to likers list
- Fix hover interaction: delay close to allow entering tooltip
- Add Escape key to close tooltip
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* fix: use data prop instead of parent() for sensitive images SSR
The +layout.ts file was incorrectly using parent() to access data from
+layout.server.ts. In SvelteKit, data from a server load function in the
same route comes via the data parameter, not parent(). parent() is only
for accessing data from parent routes.
This fixes SSR meta tag generation for sensitive images.
* fix: handle nullable data parameter in layout load function
TypeScript was complaining that data could be null. Use optional
chaining and extract sensitiveImages to a variable to avoid repetition.
Link previews weren't filtering sensitive images because the moderation
data was only fetched client-side. Now we fetch it in +layout.server.ts
and pass it through to pages for SSR-safe meta tag filtering.
- Add +layout.server.ts to fetch sensitive images
- Export checkImageSensitive() utility for shared use
- Update track and artist pages to use SSR-safe checks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Was blocking all Bluesky avatars. Keep only explicit flagging.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: blur explicit images with user opt-in to show
- add explicit_images table to track flagged image URLs/IDs
- add show_explicit_artwork user preference (default: hidden)
- add /moderation/explicit-images endpoint to fetch flagged images
- add ExplicitImage component that blurs flagged images
- hide explicit images from og:image/twitter meta tags
- add portal toggle for explicit artwork preference
images can be flagged by image_id (R2) or full URL (external avatars).
frontend fetches the list once and checks all rendered images.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: use 'sensitive' instead of 'explicit' in tooltip
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: rename explicit to sensitive throughout codebase
- Rename table: explicit_images → sensitive_images
- Rename model: ExplicitImage → SensitiveImage
- Rename preference: show_explicit_artwork → show_sensitive_artwork
- Rename component: ExplicitImage.svelte → SensitiveImage.svelte
- Update all references, endpoints, and UI text
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
added note about intermittent audio playback hangs in iOS standalone
(PWA) mode. PR #466 added NetworkOnly caching for audio routes, but
iOS Safari is slow to update service workers. workaround is to delete
and re-add home screen bookmark. flagged for further investigation if
issue persists.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Space: play/pause
- Left/Right arrows: seek ±10 seconds
- J/L: previous/next track
- M: mute/unmute
All shortcuts respect input focus and are disabled when search modal is open.
Also fixes AGENTS.md to reflect that STATUS.md is now tracked.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
terminal-based vanity metrics dashboard using plotext for visualization.
features:
- zone stats: requests, pageviews, unique visitors, bandwidth, cache ratio
- daily requests bar chart (cyan)
- pageviews vs uniques line chart (green/magenta)
- CLI options: --days/-d for time window, --no-cache to force refresh
- automatic daily caching (~/.cache/plyr-analytics/)
- pydantic-settings for .env integration (CF_API_TOKEN, CF_ZONE_ID)
usage:
uv run scripts/cf_analytics.py # last 7 days
uv run scripts/cf_analytics.py -d 30 # last 30 days
uv run scripts/cf_analytics.py --no-cache
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the $effect that calls incrementPlayCount() runs on every currentTime
update (~4x/second). when threshold is first crossed, svelte's batched
reactive updates meant the guard (playCountedForTrack === currentTrack.id)
could miss rapid-fire calls, causing 2 scrobbles ~50-125ms apart.
fix: add synchronous (non-reactive) guard that blocks immediately,
before the async fetch fires.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add mutagen dependency for audio metadata extraction
- create backend/utilities/audio.py with extract_duration()
- extract duration during upload, store in track.extra["duration"]
- add backfill script for tracks uploaded before this feature
duration is now correctly passed to teal.fm scrobble records.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- add Dec 4 section for PR #467 (teal.fm scrobbling)
- document lexicons, configuration, and code quality improvements
- update now-playing section to reflect native scrobbling
- add teal.fm to "what's working" list
- update last modified date
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* checkpoint
* checkpoint
* feat: teal.fm scrobbling integration
adds teal.fm scrobbling support - when users enable the toggle in settings
and re-authenticate, their plays are recorded to their PDS using teal's
ATProto lexicons.
changes:
- add TealSettings config class for configurable namespaces (TEAL_PLAY_COLLECTION,
TEAL_STATUS_COLLECTION, TEAL_ENABLED env vars)
- fix play count fetch missing credentials: 'include' (root cause of scrobbles
not triggering)
- add preferences.fetch() after login to ensure teal toggle state is current
- add unit tests for env var overrides (proves we can adapt when teal graduates
from alpha namespace)
the teal namespace defaults to fm.teal.alpha.* but can be changed via env vars
when teal.fm updates their lexicons.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: code quality improvements and bug fixes
- walrus operators throughout auth and playback code
- extract bearer token parsing into utilities/auth.py
- move DeveloperTokenInfo to top of file with field_validator for truncation
- fix now-playing firing every 1s (update lastReportedState in scheduled reports)
- use settings.frontend.url/domain for origin URLs (environment-aware)
- move teal.fm scrobbling setting from gear menu to portal "Your Data" section
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
audio streaming was intermittently hanging in iOS standalone (PWA) mode.
the service worker was intercepting /audio/{file_id} requests with
NetworkFirst caching, which caused issues with:
- 307 redirects to R2 CDN getting cached/stale
- range request headers not being handled properly on iOS Safari
switched to NetworkOnly for audio routes so the SW passes through
without interference. we weren't actually caching audio files anyway
(just the redirect response), so there's no functional loss.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
The search modal was using `visibility: hidden` to hide the backdrop,
which prevented the input from receiving focus. This caused:
- No text cursor on desktop when pressing Cmd+K
- No keyboard popup on mobile when tapping search icon
Changed to use only `opacity: 0` (already present) for hiding.
Elements with `opacity: 0` remain focusable, allowing the synchronous
focus() call in search.open() to work correctly - critical for mobile
keyboards which only open when focus is in the same call stack as the
user gesture.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the now-playing endpoints were hitting the global 100/minute rate limit
when multiple users/tabs were active simultaneously. since these endpoints
are already throttled client-side (10-second intervals, 1-second debounce,
5-second progress buckets), server-side rate limiting is unnecessary.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
the documented commands didn't match actual justfile recipes:
- `just frontend install` -> doesn't exist
- `just run-backend` -> `just backend run`
- `just lint` -> `just backend lint` / `just frontend check`
- `just migrate` -> `just backend migrate`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
when a track doesn't have its own artwork but belongs to an album
with artwork, use the album artwork instead of the placeholder icon.
applies to:
- player artwork thumbnail (TrackInfo.svelte)
- media session metadata for system controls (Player.svelte)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
iOS/mobile browsers only open keyboard when focus() is called directly
in a user gesture handler. Previously, focus happened in a Svelte $effect
after state change, which broke the gesture chain.
Fix: Always render SearchModal (hidden via CSS), register input ref with
search state, and focus directly in search.open() before state change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: move search to top-level mobile header
- Add search icon button to mobile-center nav (left of feed/liked icons)
- Remove search from ProfileMenu dropdown
- Space icons evenly across mobile header
- Cleaner mobile UX with one less tap to search
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: hide keyboard hints on mobile, smaller placeholder text
- Hide Cmd+K shortcut badge on mobile
- Hide keyboard navigation hints (arrows, esc) on mobile
- Shrink placeholder text on mobile to prevent overflow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: collapse excess tags with +N button and fix mobile menu overlap
- limits visible tags to 2 by default (never wraps to second line)
- shows "+N" button when more tags exist
- clicking "+N" expands to show all tags (allows wrapping when expanded)
- uses flex-wrap: nowrap + overflow: hidden to guarantee single-line constraint
- collapses back when track changes (component recycle)
- fix LinksMenu and ProfileMenu mobile positioning to avoid player overlap
- menus now position from bottom with player height offset on mobile
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: keep mobile menus centered but shift up for player
- menus stay full-size and centered
- shift center point up by half player height when player is open
- cap max-height to avoid overlap with player
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add search trigger to header and mobile menu
desktop:
- SearchTrigger component shows magnifying glass + keyboard shortcut hint
- detects platform for correct shortcut (⌘K on Mac, Ctrl+K on Windows/Linux)
- subtle styling with accent highlight on hover
mobile:
- search added as first item in ProfileMenu (three-dot menu)
- styled with accent tint background to stand out
- opens search modal and closes menu
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: hide portal link on portal page, move search to far right
- portal link (@handle) now hidden when already on /portal
- search trigger moved after logout button (far right)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: reorder desktop header - search in nav, logout on far right
- search trigger now in nav: @handle → search → settings
- logout button moved to far right (mirroring stats on left)
- hide portal link when already on /portal page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: swap search and portal link order in nav
nav order now: feed → liked → search → @handle → settings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* more stuff
* yuhhhhhhhh
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: collapse excess tags with +N button and fix mobile menu overlap
- limits visible tags to 2 by default (never wraps to second line)
- shows "+N" button when more tags exist
- clicking "+N" expands to show all tags (allows wrapping when expanded)
- uses flex-wrap: nowrap + overflow: hidden to guarantee single-line constraint
- collapses back when track changes (component recycle)
- fix LinksMenu and ProfileMenu mobile positioning to avoid player overlap
- menus now position from bottom with player height offset on mobile
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: keep mobile menus centered but shift up for player
- menus stay full-size and centered
- shift center point up by half player height when player is open
- cap max-height to avoid overlap with player
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
trigram similarity alone misses cases where a short query
(e.g. "real") is a substring of a word in a long title.
added OR condition with ILIKE to catch exact substring matches
while preserving fuzzy matching behavior for typos.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
replaced text characters (♪, ◉, ◫, #) with actual SVG icons
matching those used elsewhere in the app:
- track: music note icon (same as player placeholder)
- artist: person icon (same as TrackItem/TrackInfo)
- album: record icon (same as TrackItem/TrackInfo)
- tag: label icon (same as tag detail page)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
changed conditional from `commentsEnabled !== false` to
`commentsEnabled === true` so the section only renders
after we explicitly know comments are enabled.
previously: commentsEnabled started as null, which passed
the !== false check, causing a brief flash before the API
response set it to false.
note: no regression test added - frontend lacks test infrastructure.
the fix is a one-line conditional change with clear before/after behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* docs: add unified search documentation
- update keyboard-shortcuts.md with Cmd/Ctrl+K search shortcut
- create comprehensive search.md covering frontend state, backend API,
database indexes (pg_trgm), fuzzy matching, and scaling considerations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add unified search to STATUS.md
- add recent work section for unified search (PR #447)
- move issue #440 from new features to working features
- add search docs to documentation links
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: unified search with Cmd/Ctrl+K shortcut
- add pg_trgm extension and GIN indexes for fuzzy search
- implement /search endpoint with trigram similarity scoring
- search across tracks, artists, albums, and tags
- create SearchModal component with keyboard navigation
- Cmd+K (Mac) / Ctrl+K (Windows/Linux) opens search
- arrow keys navigate, Enter selects, Esc closes
- results grouped by type with relevance scoring
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: improve search modal UX and styling
- use "Cmd+K" text instead of ⌘ symbol for clarity
- add client-side query length validation (max 100 chars)
- show clear error message when query exceeds limit
- apply glassmorphism styling to modal:
- frosted glass background with blur
- subtle border highlights
- refined hover/selected states
- consistent translucent elements
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: show artwork/avatars in search results
- display track artwork, artist avatars, album covers when available
- lazy loading with graceful fallback to icon on error
- image hidden on load failure, icon shown instead
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
STATUS.md:
- add theme system (PR #441): CSS custom properties, dark/light/system toggle
- add mobile UX improvements (PR #443): ProfileMenu, /upload page, portal overhaul
- add player scroll timing fix (PR #445): faster animations, "view track" link
- add CI optimization (PR #444): path-based hook skipping
- add issue #440 to new features list (unified search)
- update last modified date
docs/frontend/toast-notifications.md:
- document action links feature (was listed as "not implemented")
docs/frontend/state-management.md:
- update preferences to include theme and hidden tags
- mention ProfileMenu component for mobile
- fix upload flow to reference /upload page instead of portal
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
Player scroll:
- remove the 1.5s hold at -100% (off-screen) position
- text now immediately resets after scrolling off
- reduce title animation from 10s to 8s
- reduce artist/album animation from 15s to 10s
Upload toast:
- fix duplicate "track uploaded successfully" toast
(was showing once on XHR complete, again on SSE complete)
- add "view track" link to success toast with track detail page
- toast action links now stay in same tab for internal URLs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
use dorny/paths-filter to detect what changed and only install
deps that are actually needed:
- frontend changes: install bun + frontend deps
- backend changes: install uv + backend deps
- other changes: no extra deps needed
this avoids installing all deps for every PR, reducing CI time
for frontend-only or backend-only changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Add ProfileMenu component for mobile (collapses profile/upload/settings/logout)
- Extract upload form to dedicated /upload page
- Standardize section headers across home and liked pages
- Comprehensive mobile styling for portal page:
- Tighten profile settings form
- Fix track edit/delete icon alignment with proper SVG icons
- Clean up data section typography
- Add track detail link under artwork
- Fix create token button alignment
- ProfileMenu hides contextual links (profile hidden on /portal, upload hidden on /upload)
- Upload card design in portal links to /upload page
- Fix layout inconsistencies between home and liked pages (padding, font-weight, mobile styles)
- Add globe icon in mobile header to navigate home when not on home page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
- Replace hardcoded colors across 35 files with CSS custom properties
- Add theme switcher (dark/light/system) in settings menu
- Define semantic color tokens: bg-*, border-*, text-*, accent-*
- Light theme adapts all UI elements including tracks, header, tags
- Remove zen mode feature (was only hiding a few elements)
- Use color-mix() for derived colors to maintain consistency
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
when a track has the 'bufo' tag, semantically-matched toad GIFs
float across the track detail page. uses the track title as a
semantic search query against the find-bufo API.
- BufoEasterEgg component fetches toads based on track title
- results cached in localStorage for 1 week to reduce API calls
- TagEffects wrapper provides extensibility for future tag-based plugins
- animated toads drift across viewport with wobble effects
- respects prefers-reduced-motion
- fails gracefully if API is unavailable
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* chore: add ai tag to status maintenance uploads
uses plyrfm >= v0.0.1-alpha.10 which supports -t/--tag option
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update STATUS.md with tag filtering and SDK work
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
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>