Commit Graph
100 Commits
Author SHA1 Message Date
vicrodh 1fae9b7d9f fix(ui): wire active state in lists
cx
2026-01-19 13:02:43 -06:00
vicrodh c59ab226b9 fix(playback): honor autoplay and seed queues
cx
2026-01-19 12:46:28 -06:00
vicrodh 7c4fb6d483 fix(favorites): show active track and icon
cx
2026-01-19 12:42:27 -06:00
vicrodh c97d4680e4 feat(theme): add additional palettes
cx
2026-01-19 12:41:43 -06:00
vicrodh 05780f2b13 refactor(theme): centralize global theme styles
cx
2026-01-19 11:43:14 -06:00
vicrodh 395fd165d4 feat(player): add volume sliders to FocusMode and MiniPlayer
cx
2026-01-19 11:26:08 -06:00
vicrodh 318e3d795a feat(context): scroll to active track on context navigation
cx
2026-01-19 11:19:52 -06:00
vicrodh 78c4898edf fix(favorites): persist custom icon and sync modal state
cx
2026-01-19 11:11:17 -06:00
vicrodh 7831b07c30 feat(favorites): make tabs history-aware views and normalize order
cx
2026-01-19 11:11:03 -06:00
vicrodh b82abb145b fix: resolve Favorites tab navigation circular bug and prevent user clicks from triggering nav effect
cx
2026-01-18 16:32:13 -06:00
vicrodh 9c45a82fd8 fix(favorites): add debug logging for preferences loading
cp
2026-01-18 16:21:36 -06:00
vicrodh e752791d3a fix: correct async loading of favorites preferences and tab order
- Remove await from onMount (not async)
- Use .then() chain for proper async handling
- Initialize activeTab from preferences on mount
- Fixes tab order not persisting
- Fixes custom icon not displaying

cx
2026-01-18 16:17:30 -06:00
vicrodh 5e60de9970 fix(phase4): await favorites preferences load to preserve tab order
cp
2026-01-18 16:07:56 -06:00
vicrodh 4a6f09d5b9 fix(favorites): restore tab navigation functionality
Removed continuous  sync that was overriding user tab clicks.
Now only syncs activeTab with currentTab on initial mount to respect
configured first tab, then allows normal tab navigation.

cp
2026-01-18 16:02:10 -06:00
vicrodh ffb16656e7 feat(phase4.7): implement history-aware favorites tab navigation
- Extended navigationStore with FavoritesTab state tracking
- Added setFavoritesTab() to push tab changes to history
- Modified navigateTo() to support favorites sub-navigation
- Wired handleTabChange() to call setFavoritesTab()
- Synced activeTab with navigation store via currentTab prop
- Back button now returns to correct Favorites tab

cp
2026-01-18 15:52:31 -06:00
vicrodh 902705f47f fix(phase4): add icon_background column migration for existing databases
- Detect missing icon_background column in favorites_preferences table
- Add ALTER TABLE migration to preserve existing user data
- Apply migration in both FavoritesPreferencesStore::new() and create_table()

cx
2026-01-18 15:42:11 -06:00
vicrodh ad73b5d879 fix(phase4): adjust sticky nav top position to reach window edge
- Change top from 0 to -24px to account for container padding
- Ensures nav bar reaches top edge when scrolling
- clearCurrentSearch already collapses search correctly

cp
2026-01-18 15:31:44 -06:00
vicrodh 92486c3943 fix(phase4): sticky nav reaches top edge and search collapses on clear
- Remove negative top margin from .favorites-nav (was -24px, now 0)
- Update clearCurrentSearch() to collapse search when cleared
- Add hover state to clear button for better UX

cp
2026-01-18 15:28:07 -06:00
vicrodh a45806e4c4 fix(phase4): sticky nav now reaches top edge when scrolling
- Adjusted negative margins to cancel parent padding
- Matches Artist page sticky behavior
- Nav bar now fully sticks to window top

cp
2026-01-18 15:20:40 -06:00
vicrodh 88e0ef8e01 feat(phase4.5): implement Artist-style navigation bar
- Replaced button-style tabs with underlined navigation links
- Sticky navigation bar on scroll (top: 0, z-index: 4)
- Integrated searchbar in nav-right (icon → expandable input)
- Tab order respects user preferences (favoritesPreferences.tab_order)
- Active tab shows accent-colored bottom border (2px)
- Removed redundant search container section
- Matches Artist page and Settings page nav styles

cp
2026-01-18 15:12:58 -06:00
vicrodh 9269458d77 feat(phase4): add helper functions for tab navigation and search
- Added getCurrentSearchValue(), setCurrentSearchValue(), clearCurrentSearch()
- Added getTabIcon() and getTabLabel() helpers
- Added tabLabels constant
- Added searchExpanded state for collapsible search
- Preparation for Artist-style navigation bar (Phase 4.5)

cp
2026-01-18 15:11:42 -06:00
vicrodh 64bd4d729e feat(phase4.4): wire custom icon and background display
- Dynamic icon rendering (preset + custom upload)
- Background color/gradient applied to header icon
- Support for all preset icons (heart, star, music, folder, disc, library)
- Custom uploaded image display via asset protocol

cp
2026-01-18 15:08:44 -06:00
vicrodh 76c9a9653e feat(phase4.3): add custom gradient picker with 2-3 color support
- Expanded preset gradients (sunset, ocean, fire, mint, candy, forest)
- Added custom gradient builder (2-3 colors)
- Interactive color picker with add/remove controls
- Apply/Cancel actions for custom gradients

cp
2026-01-18 15:07:21 -06:00
vicrodh 3039cb5d37 fix(phase4.3): expand color grid to 12 columns with additional colors
- Add 3 more solid colors (cyan, rose, slate) for total of 11 solids
- Keep only sunset gradient (1 gradient)
- Change grid from 8 to 12 columns
- Ensures all colors fit in single row

cp
2026-01-18 15:02:28 -06:00
vicrodh df50eb1c8f fix(ui): reduce color grid gap to 2px
cp
2026-01-18 14:46:11 -06:00
vicrodh 2f163f36f5 fix(phase4.3): adjust modal centering and color swatch sizes
- Reduce modal width from 920px to 736px (80% of original)
- Center modal with 140px left offset (half sidebar width)
- Reduce color swatches from 40px to 30px
- Reduce color-grid gap from 8px to 4px
- Fix icon-preset button justify-content typo
- Swatches now properly fill single row

cp
2026-01-18 14:44:29 -06:00
vicrodh cdc4435306 fix(phase4.3): adjust modal width to 920px and limit color swatches to 40x40
- Reduce modal width by 20% (1150px → 920px)
- Add max-width/max-height constraints to color buttons (40px)
- Maintain aspect ratio and grid layout

cp
2026-01-18 14:37:56 -06:00
vicrodh 8607729013 fix(ui): adjust modal layout - 65/35 column split, smaller color swatches
- Changed modal columns from 50/50 to 65/35 split
- Reduced color picker grid gaps and increased density (8 columns)
- Adjusted color swatch border radius for consistency

cp
2026-01-18 14:19:51 -06:00
vicrodh 4009ae9b26 feat(phase4.3): add background color picker to favorites modal
- Add icon_background field to backend (Rust + TS types)
- Update modal to 60% wider (1150px)
- Implement two-column layout (icon left, tabs right)
- Add solid color picker (8 colors + none)
- Add gradient picker (6 gradients)
- Remove background from edit button to match PlaylistView style
- Update database migration for icon_background field

cp
2026-01-18 14:13:50 -06:00
vicrodh 5bd52cc89b feat(phase4.3): add favorites edit modal component
- Create FavoritesEditModal with preset icon grid
- Add custom image upload button
- Add tab reorder UI with up/down buttons
- Integrate edit button in Favorites page header (top-right)
- Add i18n keys for EN and ES
- Wire modal save handler to backend

cc
2026-01-18 14:02:37 -06:00
vicrodh a6aa6aa936 feat(phase4.2): add favorites preferences backend
- Create favorites_preferences.rs with FavoritesPreferences struct
- Add database table (custom_icon_path, custom_icon_preset, tab_order)
- Implement State wrapper and Tauri commands
- Register state and commands in lib.rs
- Default preset: heart, default order: tracks/albums/artists/playlists

cc
2026-01-18 13:57:17 -06:00
vicrodh d39180bd6c feat(ui): show playing status in search and artist tracks
cx
2026-01-18 05:46:57 -06:00
vicrodh c6f86a9bcc feat(ui): pause from track row playing icon
cx
2026-01-18 05:46:43 -06:00
vicrodh 407fab44db fix(ui): slow album card ticker speed
cx
2026-01-18 05:38:41 -06:00
vicrodh ed45fa4156 feat(ui): show volume value while dragging
cx
2026-01-18 05:38:32 -06:00
vicrodh 5a42df35a4 fix(ui): lock playlist import button after success
cx
2026-01-18 05:38:22 -06:00
vicrodh c67c479bac fix(ui): wire stack icon click and tooltip
cx
2026-01-18 05:33:00 -06:00
vicrodh c2628b0076 fix(config): align autoplay mode serialization
cx
2026-01-18 05:32:49 -06:00
vicrodh d96233730b fix(phase3): custom tooltip for context icon
- Replace native title with custom tooltip similar to DAC/EXC badges
- Add hover state and styled tooltip with context info
- Add debug logs to investigate onClick callback issue

cp
2026-01-18 04:58:35 -06:00
vicrodh 12067d7a61 fix(phase3): context navigation and autoplay persistence
- Fixed context navigation using correct property names (.type, .id)
- Simplified autoplay setting from radio buttons to toggle switch
- Fixed tooltip using native title attribute (like QualityBadge)
- Removed custom tooltip CSS and hover state
- Updated translations (en/es) for clearer autoplay behavior description

cp
2026-01-18 04:53:43 -06:00
vicrodh 315bdce79c fix(settings): reload playback preferences when view renders
- Added $effect to reload preferences each time SettingsView is visible
- Fixes autoplay mode not persisting when navigating away and back
- Settings view doesn't unmount, so onMount only runs once per session

cp
2026-01-18 04:38:15 -06:00
vicrodh b666722b98 debug: add detailed logging to loadPlaybackPreferences
cp
2026-01-18 04:34:54 -06:00
vicrodh 92f1644601 fix(context): add custom tooltip and pass onClick callback
- Implemented custom tooltip for StackIcon (similar to AudioOutputBadges)
- Shows 'Playing from', context name, and 'Click to navigate' hint
- Added onContextClick={handleContextNavigation} to ExpandedPlayer
- Fixed onClick prop not being passed, enabling navigation

cp
2026-01-18 04:34:42 -06:00
vicrodh 0feb1e1b4a debug: add detailed logging to StackIcon click handler
cp
2026-01-18 04:09:31 -06:00
vicrodh ae3a90d378 fix(i18n): add translations for playback preferences
Added translations for:
- Autoplay behavior setting
- Continue within source / Play only selected track options
- Show context icon setting and tooltip

Both English and Spanish translations added.

Changed StackIcon tooltip to use native browser title (matches QualityBadge style).
Removed arrow indicator from context icon (cursor change is sufficient).

cp
2026-01-18 04:09:20 -06:00
vicrodh 2a4f58cb08 fix(css): remove extra closing brace in ExpandedPlayer styles
Syntax error on line 568 - duplicate closing brace after .album rule

cp
2026-01-18 04:00:25 -06:00
vicrodh fdf288b01e fix(migration): robust database migration for show_context_icon
Problem:
- Users updating from previous versions would crash on startup
- Table created with new column caused "no column named show_context_icon" error
- Existing users had old schema without the column

Solution:
- Step 1: Create table with old schema (backwards compatible)
- Step 2: Check if show_context_icon column exists using pragma_table_info
- Step 3: Add column ONLY if it doesn't exist (safe migration)
- Step 4: Insert default row

This ensures:
✅ New installs work (table created fresh)
✅ Existing users migrate seamlessly (column added, settings preserved)
✅ No data loss
✅ Idempotent (can run multiple times safely)

Tested scenarios:
- Fresh install (no DB) → creates table, adds column, inserts defaults
  Result: id=1, autoplay_mode='continue', show_context_icon=1
- Existing DB without column → adds column, preserves user settings
  Result: id=1, autoplay_mode='track_only' (preserved), show_context_icon=1
- Existing DB with column → no changes (idempotent)

Migration logs added for debugging.

cp
2026-01-18 03:57:28 -06:00
vicrodh 584650fee1 feat(phase37): implement clickable context tooltip navigation
StackIcon Component:
- Now accepts onClick prop
- Changed from div to button element
- Added arrow (→) that appears on hover
- Tooltip text changed to "Click to go to: {source}"
- Visual feedback: hover background + active scale

Navigation Logic (+page.svelte):
- Added handleContextNavigation function
- Switch based on context_type:
  - album → handleAlbumClick
  - playlist → navigate + set selectedPlaylistId
  - artist_top → handleArtistClick
  - favorites → navigate to favorites
  - home_list → navigate to home
  - search → navigate to search
- Import getCurrentContext from playbackContextStore

UI Integration:
- ExpandedPlayer: Added onContextClick prop, passed to StackIcon
- FocusMode: Added onContextClick prop, passed to StackIcon
- Both receive handleContextNavigation callback

Phase 3.7 COMPLETE ✅

cp
2026-01-18 03:47:11 -06:00
vicrodh b070884ee8 feat(phase36): implement context icon visibility toggle (frontend + UI)
Frontend:
- Add show_context_icon to PlaybackPreferences interface
- Add setShowContextIcon function to store
- StackIcon now subscribes to preferences and checks show_context_icon
- Icon hidden when setting is false (context still tracked)

UI (Settings → Playback):
- Add toggle: "Show track playing context in player"
- Tooltip with bilingual explanation (EN/ES)
- Default: enabled (true)
- Persists across restarts

Context tracking ALWAYS active regardless of icon visibility.

Phase 3.6 COMPLETE ✅

cp
2026-01-18 03:40:19 -06:00
vicrodh dfd1eb588d feat(phase36): add show_context_icon setting (backend)
Database:
- Add show_context_icon column (INTEGER, default 1)
- Migration for existing databases

Backend:
- Add show_context_icon to PlaybackPreferences struct
- Default value: true (icon visible)
- Add set_show_context_icon method to store/state
- Add Tauri command: set_show_context_icon

Context tracking remains ALWAYS active regardless of icon visibility.

cp
2026-01-18 03:38:14 -06:00
vicrodh 640cf5db41 feat(phase35): implement context in HomeView
Context creation:
- Creates 'home_list' context for Continue Listening section
- Uses 'continue_listening' as ID and 'Continue Listening' as label
- Tracks user's listening history in a context
- Stack icon will show "Playing from: Continue Listening"

Home view now fully integrated with context system.

cp
2026-01-18 03:34:50 -06:00
vicrodh a473c969d9 feat(phase35): implement context in SearchView
Context creation:
- Creates 'search' context when playing track from results
- Uses search query as ID and 'Search: {query}' as label
- Applies to both All tab (first 6) and Tracks tab (full list)
- Stack icon will show "Playing from: Search · {query}"

Search view now fully integrated with context system.

cp
2026-01-18 03:33:23 -06:00
vicrodh e3ce6a3760 feat(phase35): implement context in FavoritesView
Context creation:
- Creates 'favorites' context when playing track
- Uses 'favorites' as ID and 'Favorites' as label
- Uses filteredTracks for context (respects search/filter)
- Stack icon will show "Playing from: Favorites"

Favorites view now fully integrated with context system.

cp
2026-01-18 03:31:25 -06:00
vicrodh 0bd60bbc8f feat(phase35): implement context in ArtistDetailView
Context creation:
- Creates 'artist_top' context when playing from Top Tracks
- Uses artist ID and name
- Passes track index from UI loop
- Stack icon will show "Playing from: Artist · {name}"

Artist view now fully integrated with context system.

cp
2026-01-18 03:30:47 -06:00
vicrodh ae478ddeb2 feat(phase35): implement context in PlaylistDetailView
Context creation:
- Creates 'playlist' context when playing track
- Uses playlist ID and name
- Filters out local tracks (Qobuz tracks only in context)
- Finds track index in Qobuz tracks list
- Stack icon will show "Playing from: Playlist · {name}"

Playlist view now fully integrated with context system.

cp
2026-01-18 03:29:33 -06:00
vicrodh fe80896fd2 feat(phase35): add helper for context creation with DisplayTrack
Added createContextAndPlayDisplayTrack helper that:
- Creates playback context with provided type/id/label/tracks
- Calls handleDisplayTrackPlay to start playback
- Will be used by Playlist, Artist, Favorites, Home views

Next: Implement context creation in each view component internally
(following AlbumDetailView pattern but keeping logic in views)

cp
2026-01-18 03:21:21 -06:00
vicrodh 7907b695f8 feat(ui): complete stack icon repositioning (all 4 players)
Stack icon now appears in artist/album area across all players:

- FocusMode: icon before artist (size 12px)
- MiniPlayer: icon at start of artist-album row (size 12px)
- Consistent sizing: 12px (compact players), 16px (expanded player)
- Visual hierarchy: Title → [icon] Artist · Album

All 4 players updated and consistent.

cp
2026-01-18 03:16:38 -06:00
vicrodh 2071149747 feat(ui): move stack icon to artist/album area
Icon now appears before artist/album metadata instead of title.

Changes:
- NowPlayingBar: icon in song-meta row (size 12px)
- ExpandedPlayer: icon in new artist-album-row with separator (size 16px)
- Removed song-title-row wrapper (not needed)
- Icon placement: [icon] Artist · Album

Remaining: FocusMode and MiniPlayer (next commit)

cp
2026-01-18 03:15:49 -06:00
vicrodh 17cd6f0ae2 fix(logic): context creation should NOT depend on autoplay setting
CRITICAL LOGIC FIX:

The autoplay setting was misunderstood. Corrected behavior:

BEFORE (wrong):
- Setting controls whether context is created
- 'continue' = create context
- 'track_only' = clear context

AFTER (correct):
- Context is ALWAYS created when playing from a source (album/playlist)
- Stack icon ALWAYS appears
- Setting only affects what happens at END of track:
  - 'continue' = play next track in context
  - 'track_only' = stop after current track

Why this makes sense:
- You're playing from an album = that's the context
- Stack icon shows the origin (informative)
- Setting controls continuation behavior (not context existence)

Also:
- Added logging to handleAutoplayModeChange
- Will investigate why setting doesn't persist

cp
2026-01-18 03:09:57 -06:00
vicrodh f69a7ab461 fix(ui): fix settings layout and add detailed logging
UI Fix:
- Add :has(.radio-group) rule to allow flexible height
- Set align-items: flex-start for radio group rows
- Prevents radio buttons from being squashed

Logging Enhancement:
- Log autoplay_mode preference value
- Log track index and total tracks
- Log when no album tracks found
- Will help diagnose why context isn't being created

cp
2026-01-18 01:53:21 -06:00
vicrodh f2891c2071 fix(wiring): move context logic to parent (+page.svelte)
CRITICAL FIX: Context was being cleared instead of created.

Root cause:
- AlbumDetailView had internal handlers but used parent's onTrackPlay
- Parent's handleAlbumTrackPlay had no context logic
- Result: context always cleared (default behavior)

Solution:
- Moved context creation logic to handleAlbumTrackPlay in +page.svelte
- Checks autoplay_mode preference
- Creates context if 'continue', clears if 'track_only'
- Removed duplicate logic from AlbumDetailView (was never executed)

Now when clicking a track:
1. handleAlbumTrackPlay checks preference
2. Creates/clears context accordingly
3. Stack icon should appear/disappear correctly

Test with default setting ('continue'):
- Click any track → should see context creation logs
- Stack icon should appear immediately

cp
2026-01-18 01:39:01 -06:00
vicrodh 3851fecd92 fix(ui): improve StackIcon reactivity and add debugging
- Replace onMount/onDestroy with $effect rune for better reactivity
- Add console logging to trace context changes
- Log when setPlaybackContext/clearPlaybackContext are called
- Log when StackIcon receives context updates

This will help debug why stack icon might not appear immediately.

To test:
1. Open browser console
2. Click a track in an album
3. Check logs for '[PlaybackContext]' and '[StackIcon]'
4. Verify context is being set and icon is receiving updates

cp
2026-01-18 01:26:18 -06:00
vicrodh b6001b0203 feat(wiring): complete playback context integration in AlbumDetailView
EXAMPLE IMPLEMENTATION showing how to wire context system:

Context Creation:
- createAlbumContext(startIndex) helper creates context with track list
- handlePlayAll() creates context from first track
- handlePlayFromHere() creates context from selected track (explicit)

Autoplay Logic:
- handleTrackPlay() checks user preference (implicit action)
  - If 'continue': creates context (play within album)
  - If 'track_only': clears context (isolated playback)

Menu Actions (Explicit - Override Setting):
- onPlayTrackOnly: always clears context
- onPlayFromHere: always creates context from track
- onPlayNow: respects autoplay setting

Precedence Rule Enforced:
- Menu actions (explicit) override global autoplay setting
- Simple clicks (implicit) respect autoplay setting

Stack Icon Integration:
- Context automatically tracked by backend
- Stack icon appears when context active
- Tooltip shows 'Playing from: Album · {album.title}'

This pattern should be replicated in:
- PlaylistDetailView
- ArtistDetailView (Top Tracks)
- FavoritesView
- HomeView (curated lists)

cp
2026-01-18 01:01:32 -06:00
vicrodh 9d4a269d36 feat(init): initialize playback context and preferences stores
- Call initPlaybackContextStore() on app mount
- Call initPlaybackPreferences() on app mount
- Load backend state into frontend stores
- Phase 3 backend + UI complete
- Wiring for track actions pending (will be done per-page)

cp
2026-01-18 00:58:26 -06:00
vicrodh 1ed541c4ac feat(ui): add autoplay mode setting in Playback section
- Add radio buttons: Continue within source / Play only selected track
- Load preference on settings mount
- Save changes immediately via handleAutoplayModeChange
- Styled with hover states and proper spacing

cp
2026-01-18 00:57:42 -06:00
vicrodh dbdd664415 feat(settings): add global autoplay mode preference (backend)
- Create PlaybackPreferences config module with SQLite persistence
- Add AutoplayMode enum (ContinueWithinSource | PlayTrackOnly)
- Register Tauri commands: get_playback_preferences, set_autoplay_mode
- Create playbackPreferencesStore for frontend state management
- Add loadPlaybackPreferences to SettingsView
- Default: Continue within source (creates context on track click)
- Settings UI pending (next commit)

cp
2026-01-18 00:49:45 -06:00
vicrodh 515632913d feat(ui): add context menu actions for playback context
- Add 'Play track only' option (clears context, single track)
- Add 'Play from here' option (sets context from current position)
- Update TrackMenu component with new actions
- Update TrackRow to pass new menu actions
- Backend integration ready for Phase 3 (settings + wiring)

cp
2026-01-18 00:41:17 -06:00
vicrodh cf5be434e3 feat(ui): add stack icon to all 4 players
- Create StackIcon component with reactive context subscription
- Display Layers icon before track title when context exists
- Show tooltip with origin info on hover
- Add to: NowPlayingBar, ExpandedPlayer, FocusMode, MiniPlayer
- Icon hidden when no playback context (single track)
- Responsive sizing per player (14px-18px)

cp
2026-01-18 00:39:25 -06:00
vicrodh 49fe9e6b11 feat(player): implement playback context system
- Add PlaybackContext module with ContextManager
- Context tracks semantic origin (album, playlist, artist top, etc.)
- Separate from queue - defines what 'next' means
- Backend: Rust struct with track IDs and position tracking
- Frontend: Svelte store with reactive subscriptions
- Tauri commands for context CRUD operations
- Context is nullable (single track playback has no context)

cp
2026-01-18 00:26:54 -06:00
vicrodh 02469aa7b2 Temporarily disable ALSA Direct backend from UI
ALSA Direct is currently non-functional due to CPAL limitations:
- CPAL cannot enumerate or open hw:X,Y devices required for bit-perfect playback
- Only named devices (front:, iec958:, sysdefault:) work, but they all use ALSA plugins with resampling
- User's DAC cannot achieve bit-perfect playback (stuck at 44.1kHz regardless of track sample rate)

Changes:
- Filter out ALSA Direct from backend selector dropdown
- Auto-switch users from ALSA Direct to Auto on settings load
- Add comments indicating this is temporary until alsa-rs rewrite

This prevents user frustration with a broken backend while we evaluate implementing
direct ALSA support via alsa-rs (bypassing CPAL entirely).
2026-01-17 23:41:04 -06:00
vicrodh b224538088 Manually create hw: and plughw: devices - CPAL doesn't enumerate them
Critical Fix:
- CPAL only enumerates named ALSA devices (front:, sysdefault:, etc.)
- CPAL does NOT enumerate hw:X,Y or plughw:X,Y devices
- These must be created manually from aplay -l card list

Solution:
- Parse aplay -l to get card numbers and descriptions
- Manually create hw:X,0 and plughw:X,0 for each card
- Assume max_sample_rate: 384000 (devices are capable)
- Description: "Cambridge Audio USB Audio 2.0 (Direct Hardware - Bit-perfect)"

For user's system (based on aplay -l):
- hw:0,0 → CS201 (Direct Hardware - Bit-perfect)
- hw:2,0 → HDA NVidia (Direct Hardware - Bit-perfect)
- hw:3,0 → sof-hda-dsp (Direct Hardware - Bit-perfect)
- hw:4,0 → Cambridge Audio USB Audio 2.0 (Direct Hardware - Bit-perfect) ← USER NEEDS THIS
- Plus plughw: variants for each

These devices will now appear in the dropdown and can be selected for
true bit-perfect playback without resampling.
2026-01-17 21:29:17 -06:00
vicrodh c93226718f Fix duplicate device names in dropdown - append device ID
UX Problem:
- 4 devices all showed "Cambridge Audio USB Audio 2.0"
- Selecting one would select all (Map key collision)
- User couldn't distinguish between hw:4,0, front:CARD=C20, iec958:CARD=C20

Solution:
- Detect duplicate display names in deviceOptions
- Append [device_id] to duplicates for uniqueness
- Example: "Cambridge Audio USB Audio 2.0 (Direct Hardware - Bit-perfect) [hw:4,0]"

Now User Sees:
- Cambridge Audio USB Audio 2.0 (Direct Hardware - Bit-perfect) [hw:4,0] ← TRUE bit-perfect
- Cambridge Audio USB Audio 2.0 [front:CARD=C20,DEV=0] ← Front output with plugins
- Cambridge Audio USB Audio 2.0 [iec958:CARD=C20,DEV=0] ← S/PDIF with plugins
- Cambridge Audio USB Audio 2.0 [sysdefault:CARD=C20] ← System default with dmix

Each option is now unique and selectable. The hw:4,0 option is clearly
marked as "Direct Hardware - Bit-perfect" for audiophiles.
2026-01-17 21:23:25 -06:00
vicrodh 39a661417d Add hw: and plughw: devices for true bit-perfect playback
ALSA Backend Changes:
- Include hw:X,Y and plughw:X,Y devices from CPAL enumeration
- These devices bypass ALL ALSA plugins (dmix, resampling, etc.)
- Parse aplay -l to build card number → description map
- hw: devices get friendly names: "Cambridge Audio USB Audio 2.0 (Direct Hardware - Bit-perfect)"

Why This Matters:
- front:CARD=C20,DEV=0 → Has dmix plugin → resamples to 44.1kHz
- iec958:CARD=C20,DEV=0 → Has plugins → resamples to 44.1kHz
- hw:4,0 → Direct hardware access → TRUE bit-perfect → 96kHz playback

Device Description Examples:
- hw:4,0 → "Cambridge Audio USB Audio 2.0 (Direct Hardware - Bit-perfect)"
- plughw:4,0 → "Cambridge Audio USB Audio 2.0 (Plugin Hardware)"

Blacklist Updates:
- Filter surround*, usbstream, pipewire, pulse, bare sysdefault
- Keep hw:, plughw:, default, sysdefault:CARD=, front:CARD=, hdmi:CARD=, iec958:CARD=

User should now select hw:4,0 device for true bit-perfect playback that
changes sample rate to match the track (96kHz, 192kHz, etc.)
2026-01-17 21:21:01 -06:00
vicrodh 845d53c672 Revert to CPAL enumeration + aplay -L for descriptions only
Critical Fix - Use CPAL Device Names:
- CPAL enumerates devices → these IDs actually work for playback
- aplay -L provides friendly descriptions → update CPAL device descriptions
- Stop creating hw:X,Y devices that CPAL doesn't recognize
- Fixes: "Device not found → fallback to default → audio from laptop"

Flow:
1. enumerate_via_cpal() returns devices CPAL can actually use
2. enumerate_via_aplay() calls #1, then parses aplay -L for descriptions
3. Updates device.description with friendly names from aplay -L
4. Frontend shows: "Cambridge Audio USB Audio 2.0" not "front:CARD=C20,DEV=0"

Loading Overlay:
- Full-screen semi-transparent overlay during device enumeration
- Animated spinner (Loader2 icon)
- "Loading audio devices... / Parsing hardware information"
- Prevents user thinking app is frozen during 4-5 second aplay parse

For Bit-Perfect Playback:
- User should select devices like front:CARD=C20,DEV=0 (CPAL knows these)
- These are more direct than sysdefault:CARD= (which has dmix/plugins)
- Not as direct as hw:4,0 but CPAL can actually find them

PipeWire and PulseAudio backends unchanged - still working as before.
2026-01-17 21:12:18 -06:00
vicrodh 63b6809259 Fix hw: device format - use card numbers not names
Critical Fix:
- Changed hw: devices from hw:CARD=C20,DEV=0 to hw:4,0
- CPAL only accepts numeric format (hw:X,Y), not named format
- Parse aplay -l to get actual card numbers and device numbers

Before: hw:CARD=C20,DEV=0 → Device not found → fallback to default
After:  hw:4,0 → CPAL finds device → bit-perfect playback works

Example from user's system:
- aplay -l shows: card 4: C20 [Cambridge Audio USB Audio 2.0], device 0
- We now create: hw:4,0 (not hw:CARD=C20,DEV=0)
- Description still shows: "Cambridge Audio USB Audio 2.0 (Direct Hardware - Bit-perfect)"

This fixes the issue where selecting the DAC device would play through
laptop speakers instead because CPAL couldn't find the device.
2026-01-17 21:07:18 -06:00
vicrodh 8f3369a583 Filter ALSA devices, add hw: bit-perfect devices, and fix UI issues
ALSA Device Filtering (45 → ~12 devices):
- Aggressively filter to show only useful devices for audiophiles
- Keep: default, sysdefault:CARD=, front:CARD=, hdmi:CARD=, iec958:CARD=
- Blacklist: surround* variants, usbstream:, pipewire, pulse, bare sysdefault
- Reduces 45 listed devices to ~12 actually useful ones

Add hw:CARD=XXX,DEV=0 Devices for Bit-Perfect Playback:
- Auto-generate hw: devices for each detected card
- hw: = direct hardware access without ALSA plugins (true bit-perfect)
- sysdefault:CARD= has dmix/resampling plugins (NOT bit-perfect)
- Description shows "(Direct Hardware - Bit-perfect)" to distinguish
- This fixes user's issue: DAC was stuck at 44.1kHz with sysdefault:CARD=C20

Loading Indicator:
- Show "Loading devices..." while parsing aplay -L output
- Fixes 4-5 second frozen UI on ALSA Direct backend selection
- isLoadingDevices state added to SettingsView

Stop Playback on Device Change:
- Call stop_playback() in handleBackendDeviceChange
- Prevents stuck/dead audio streams when changing output device
- Fixes: "After any config change, song in player stops working"

User can now:
1. Select hw:CARD=C20,DEV=0 for true bit-perfect to DAC
2. See loading indicator instead of frozen UI
3. Change device without breaking playback
4. Only see relevant devices (not 7 surround variants per card)
2026-01-17 21:02:03 -06:00
vicrodh 7091c64d56 Add alsa-utils to all package formats with appropriate handling
AUR (Arch Linux):
- Added to optdepends: 'alsa-utils: Better ALSA device names and descriptions'
- Users get notification to install, but not required

Flatpak:
- Included alsa-utils module in bundle (builds from source)
- Version 1.2.12 from alsa-project.org
- Sandboxed environment can now execute aplay -L internally
- Disabled alsamixer and xmlto (not needed, reduces size)
- Users get real device descriptions automatically

Snap (future - not yet implemented):
- Created snapcraft.yaml template
- stage-packages includes alsa-utils
- Will be bundled when Snap support is implemented

AppImage (future - not yet implemented):
- Created README.md documenting dependencies
- AppImage uses host system commands (aplay from host)
- Users need to install alsa-utils on their system
- Fallback to CPAL works if not installed

Summary by format:
- .deb/.rpm: Recommends alsa-utils (auto-installed, can be removed)
- AUR: optdepends (user notified, optional)
- Flatpak: Bundled (always available)
- Snap: Will be bundled (future)
- AppImage: Host dependency (user installs)
2026-01-17 20:04:29 -06:00
vicrodh 037b8569d6 Add fallback to CPAL enumeration if aplay -L fails
ALSA Backend Fallback:
- enumerate_via_aplay() - Preferred method with real hardware descriptions
- enumerate_via_cpal() - Fallback without descriptions (frontend will translate)
- enumerate_alsa_devices() tries aplay first, falls back to CPAL on failure
- Warning log suggests installing alsa-utils if aplay fails

Edge Case Handled:
- System has ALSA installed but not alsa-utils package
- CPAL detects ALSA host → AlsaBackend available
- aplay -L command fails → fallback to CPAL enumeration
- User sees translated names instead of real hardware descriptions
- App still works, just with less friendly device names

Package Dependencies:
- Added alsa-utils as "Recommends" for .deb packages (Debian/Ubuntu)
- Added alsa-utils as "Recommends" for .rpm packages (Fedora/RHEL)
- "Recommends" = installs by default but not required (apt/dnf behavior)
- If user explicitly removes alsa-utils, app falls back gracefully

In practice alsa-utils is almost always installed with ALSA, but this
ensures we handle the edge case properly.
2026-01-17 20:00:13 -06:00
vicrodh c871f6becd Use aplay -L for real ALSA device descriptions instead of hardcoding
ALSA Backend Changes:
- Parse aplay -L output to get real hardware descriptions
- Description from hardware: "Cambridge Audio Cambridge Audio USB Audio 2.0, USB Audio"
- No more hardcoded CARD_NAMES for specific hardware
- Still uses CPAL to verify device exists and get max sample rate
- Blacklist filtering preserved (null, lavrate, samplerate, etc.)

Frontend Changes:
- Prefer device.description (from aplay -L) when available
- Falls back to getDevicePrettyName() translation only if no description
- ALSA Direct shows: "Cambridge Audio ... USB Audio, Front output / input"
- Not: "Cambridge Audio USB Front Output" (hardcoded translation)

Removed Hardcoding:
- Removed CS201 → "Cambridge Audio USB" (was specific to user's DAC)
- Removed C20 → "Cambridge Audio"
- Generic CARD_NAMES remain (NVidia, Intel HD Audio, etc.)

This makes device names work for ANY hardware, not just known cards.
Each backend should use native tools:
- ALSA: aplay -L
- PipeWire: pw-cli/pactl (future)
- PulseAudio: pactl (future)
2026-01-17 19:55:22 -06:00
vicrodh 97fb8d332a Fix ALSA device name translation and Auto mode device selection
ALSA Device Name Translation:
- Added missing patterns: front:CARD=, surround*:CARD=, iec958:CARD=
- Now shows "Cambridge Audio USB Front Output" instead of "front CS201 Output 0"
- Updated CARD_NAMES: CS201 → "Cambridge Audio USB", C20 → "Cambridge Audio"
- Surround patterns: 40→4.0, 41→4.1, 50→5.0, 51→5.1, 71→7.1
- S/PDIF (iec958) pattern added

Auto Mode Device Selection Fix (CRITICAL):
- Auto mode now only allows "System Default" (no specific device selection)
- Fixes device mismatch issue where PipeWire device names fail in ALSA
- Previous behavior caused:
  * User selects DAC in Auto mode (PipeWire device name)
  * Player uses legacy ALSA path
  * Device not found → falls back to default
  * 18+ second delay before playback starts
- Auto mode is now truly automatic - always uses System Default

This prevents cross-backend device name incompatibility. If user wants
to select specific device, they must choose PipeWire, ALSA Direct, or
PulseAudio backend explicitly.
2026-01-17 19:52:47 -06:00
vicrodh b008f333b0 Fix smart toggles and device name translation for all backends
Smart Toggle Improvements:
- DAC Passthrough now only works with PipeWire backend (uses pw-metadata)
- Auto-disables when switching to ALSA Direct, Auto, or PulseAudio
- Exclusive Mode only works with ALSA Direct backend
- Auto-disables when switching to any other backend
- Clear tooltips explain incompatibilities

Device Name Translation:
- Auto backend now loads devices and applies translation (was showing ugly names)
- ALSA Direct always applies translation via needsTranslation()
- All backends use deviceOptions with consistent translation
- Removed separate audioDeviceOptions path that skipped translation

Playback Reset:
- Stop playback when changing backend to prevent stuck/dead streams
- Fixes issue where pressing Play on current track would hang
- Player state is fully reset when backend changes

All backends (Auto, PipeWire, ALSA Direct, PulseAudio) now consistently:
1. Show translated device names
2. Auto-disable incompatible features
3. Reset to System Default device on change
4. Stop playback to prevent stream corruption
2026-01-17 19:39:22 -06:00
vicrodh 122eac7e15 Implement smart toggles with automatic incompatibility handling
- Add DAC Passthrough auto-disable when ALSA Direct backend is selected
- DAC Passthrough toggle becomes disabled (grayed out) with ALSA Direct
- Add tooltip explaining why DAC Passthrough is disabled with ALSA Direct
- Always reset Output Device to System Default when backend changes
- Preserve existing auto-disable of Exclusive Mode with PipeWire/Auto

This ensures incompatible feature combinations are handled automatically,
respecting the user's last manual change while preventing invalid states.
2026-01-17 19:12:51 -06:00
vicrodh 4486d1dd80 Fix ALSA device enumeration and Auto backend translation
Problem 1: ALSA Direct showed NO devices because aplay -L output doesn't
match CPAL device names, causing create_output_stream to fail.

Solution: Revert to CPAL enumeration but with blacklist filtering to skip
non-playback devices (null, lavrate, samplerate, speexrate, jack, oss,
speex, upmix, vdownmix).

Problem 2: Auto backend didn't apply fancy names because needsTranslation()
was too restrictive and didn't cover all ALSA device name patterns.

Solution: Simplify to: if device name starts with 'alsa_output.' it's from
PipeWire (already has friendly name), otherwise it's ALSA (needs translation).
This covers ALL ALSA naming patterns including front:CARD=, sysdefault:CARD=,
hw:, hdmi:, iec958:, etc.

Changes:
- alsa_backend.rs: Back to CPAL with blacklist filter
- SettingsView.svelte: Simplified needsTranslation() logic
2026-01-17 18:24:18 -06:00
vicrodh fd09f5078a Fix Auto backend to show fancy device names
Problem: When Audio Backend was set to Auto, device names showed as raw
ALSA names (hw:0,0) instead of friendly names, even though the translator
exists and works for ALSA Direct.

Root cause: Translation was only applied when selectedBackend === 'ALSA Direct'.
Auto mode can use either ALSA or PipeWire depending on system, so it needs
smart translation.

Solution: Instead of checking selectedBackend, check the device NAME itself.
If it looks like a raw ALSA device (starts with hw:, plughw:, sysdefault:CARD=,
etc.), apply getDevicePrettyName(). Otherwise (PipeWire names like 'Built-in
Audio Analog Stereo'), pass through unchanged.

This works for:
- ALSA Direct: Always translates (all devices are raw ALSA)
- PipeWire: Never translates (all devices are friendly pactl names)
- Auto: Translates only if backend chose ALSA

Changes:
- Add needsTranslation() helper to detect raw ALSA device names
- Apply translation conditionally in deviceOptions and deviceByDisplayName
- Update loadAudioSettings to use same logic
2026-01-17 18:08:53 -06:00
vicrodh 01022f2eea Fix ALSA device filtering and quality badge hardware sample rate
Problem 1: ALSA backend showed only 1 device or listed plugins/virtual devices
that don't work for playback (null, lavrate, samplerate, etc.).

Solution: Filter aplay -L output to only include actual playback devices:
- Hardware devices (hw:, plughw:, sysdefault:CARD=, front:CARD=)
- System defaults (default, pipewire, pulse)
- Digital outputs (hdmi:, iec958:, surround)

Problem 2: Quality badge showed file sample rate (e.g., 44.1kHz) instead of
actual hardware sample rate (e.g., 192kHz) when DAC Passthrough was active.

Solution: ExpandedPlayer now polls hardware_sample_rate every second when
DAC Passthrough is enabled and displays the real hardware rate instead of
the file metadata rate.

Changes:
- alsa_backend.rs: Add is_valid_device() filter for playback devices
- ExpandedPlayer.svelte: Poll get_hardware_audio_status every 1s
- ExpandedPlayer.svelte: Use displaySamplingRate (hardware rate when DAC on)

This ensures the quality badge accurately reflects bit-perfect playback status.
2026-01-17 18:08:18 -06:00
vicrodh c0a5736c0f Fix Vite PostCSS parsing error for FolderSettingsModal
Add HTML comment at top of file to force Vite to reprocess correctly.
This fixes PostCSS trying to parse script tags as CSS due to corrupted
internal caching in Vite's Svelte plugin.
2026-01-17 17:58:32 -06:00
vicrodh f0c0e00e7d Fix ALSA device enumeration and UI translation logic
Problem 1: ALSA backend only showed CPAL hardware devices, missing virtual
devices like 'default', 'plughw', USB DACs, etc.

Solution: Use 'aplay -L' to enumerate ALL ALSA devices including:
- Hardware devices (hw:X,Y)
- Virtual devices (default, plughw, dmix)
- USB audio devices
- Network audio devices

Problem 2: UI was applying getDevicePrettyName() to all backends, causing
PipeWire's already-friendly names to be translated incorrectly.

Solution: Only apply translator for ALSA/Auto backends. PipeWire names
pass through unchanged since pactl already provides friendly descriptions.

Changes:
- ALSA backend: Use aplay -L instead of CPAL enumeration
- Frontend: Conditional translation based on selected backend
- Map deviceByDisplayName for correct reverse lookup
- Load settings with correct translation logic

This fixes both issues: ALSA now shows all devices including DACs,
and PipeWire names are no longer mangled.
2026-01-17 17:54:51 -06:00
vicrodh d530b13497 Fix playlist import after API keys refactor merge
Remove unused ProviderCredentials parameters from playlist import functions.
The proxy now handles all provider credentials, making these parameters
obsolete.

Changes:
- preview_public_playlist: Remove spotify_creds and tidal_creds params
- import_public_playlist: Remove spotify_creds and tidal_creds params
- Update all callers to remove None, None arguments
- Remove unused ProviderCredentials import

This fixes compilation error:
error[E0061]: this function takes 1 argument but 3 arguments were supplied
2026-01-17 17:42:49 -06:00
vicrodh 2b04d05af4 Add fancy device names for ALSA Direct backend
Apply getDevicePrettyName translator to all audio devices in settings.
This provides user-friendly names for ALSA devices (e.g., 'USB: Topping D50s'
instead of 'hw:0,0') while preserving PipeWire's native descriptions.

Changes:
- Import getDevicePrettyName from audioDeviceNames utils
- Apply translator in deviceOptions derived state
- Create deviceByPrettyName map for reverse lookup
- Update handleBackendDeviceChange to use pretty name mapping
- Update loadAudioSettings to display pretty names

This solves the issue where Auto and ALSA Direct backends showed
cryptic device names while PipeWire showed friendly descriptions.
2026-01-17 17:39:13 -06:00
vicrodh fccc7f781d Merge branch 'feature/api-keys-refactor' 2026-01-17 17:35:19 -06:00
vicrodh 676d58af0f Add 1-second polling for DAC Passthrough badge updates
User requested faster badge color changes. Previously badge only updated
on mount or hover, causing 2+ second delays.

Solution: Poll hardware status every 1 second when DAC Passthrough is
enabled. This provides instant visual feedback without affecting playback:
- Only polls when dac_passthrough is true (conditional)
- Only reads hardware status (no playback impact)
- Cleanup on component unmount
- Fast enough for instant feedback, light enough for performance

Addresses user feedback: badge updates now appear instantly instead of
requiring hover or waiting multiple seconds.
2026-01-17 17:31:37 -06:00
vicrodh 89126446bb Implement ALSA Direct backend and smart toggles for incompatible features
ALSA Direct Backend Implementation:
- Fully functional ALSA backend using CPAL's ALSA host
- Enumerates ALSA devices with hardware names
- Supports custom sample rates for bit-perfect playback
- Supports exclusive mode (device blocking)
- Smaller buffer size (512 samples) for lower latency in exclusive mode
- Clear error messages when device is in use

Smart Toggles in UI:
- Exclusive Mode auto-disabled with PipeWire/Auto backends
  (PipeWire is multiplexed and cannot provide true exclusive access)
- Gapless Playback auto-disabled with DAC Passthrough
  (bit-perfect requires recreating streams for sample rate changes)
- Toggle component now supports 'disabled' prop with visual feedback
- Tooltips updated to explain why features are disabled
- Auto-disable exclusive mode when switching to PipeWire backend

User Benefits:
- ALSA Direct now appears in backend dropdown
- True exclusive mode available with ALSA Direct
- Prevents confusing feature combinations
- Clear explanations via tooltips
- No more wondering why exclusive mode doesn't work with PipeWire

Technical Notes:
- ALSA backend uses CPAL host selection (not alsa-rs directly)
- Disabled toggles shown as grayed out with cursor: not-allowed
- Backend switching auto-adjusts incompatible settings
2026-01-17 17:21:23 -06:00
vicrodh 30e8fef7a4 Force PipeWire sample rate via pw-metadata for bit-perfect playback
Problem: PipeWire was resampling 192kHz streams to 48kHz because the sink
was running at 48kHz. Creating a stream at 192kHz wasn't enough - PipeWire
would resample it down to match the sink's current rate.

Solution: Use pw-metadata to force PipeWire's global sample rate before
creating the stream:

  pw-metadata -n settings 0 clock.force-rate 192000

This tells PipeWire to switch the sink to 192kHz, preventing resampling.

Technical details:
- Executes AFTER setting default sink
- Waits 300ms for PipeWire to apply the change
- Creates stream AFTER sample rate is forced
- Each new stream updates the forced rate (supports rate switching)

This ensures:
- 192kHz track -> 192kHz hardware (no resampling)
- 44.1kHz track -> 44.1kHz hardware (no resampling)
- True bit-perfect playback

Evidence: pw-top now shows ALSA sink running at requested sample rate
2026-01-17 17:12:24 -06:00
vicrodh cac5a0c173 Remove unused API Keys UI and infrastructure
All API requests now go through Cloudflare Workers proxy.
User-provided credentials were collected but never used.

Removed:
- src-tauri/src/api_keys.rs (entire module)
- API Keys section from Settings UI
- ApiKeysState and related commands
- User credential parameters from providers
- Simplified DiscogsClient (removed with_user_credentials)
- Cleaned up imports and translations

Moved documentation:
- CHANGELOG-api-keys-refactor.md
- HANDOFF-MERGE.md
- MERGE-PREP.md
- POST-MERGE-CLEANUP.md
Moved to: qbz-nix-docs/feature-api-keys-refactor/

This cleanup reduces codebase by ~715 lines and removes
confusing UI that promised functionality that didn't work.
2026-01-17 17:09:49 -06:00
vicrodh 7631462522 Update HANDOFF to reflect workflow changes and merge status
Clarified that previous 14 commits are already in main.
This merge only adds workflow cleanup (remove API key injection).
2026-01-17 16:56:57 -06:00
vicrodh b182d2d6d5 Remove API key injection from GitHub workflows
All API credentials are now managed server-side through the Cloudflare
Workers proxy. Build workflows no longer need to inject secrets into
the binary.

Removed:
- SPOTIFY_API_CLIENT_ID/SECRET from release-linux.yml
- TIDAL_API_CLIENT_ID/SECRET from release-linux.yml
- DISCOGS_API_CLIENT_KEY/SECRET from release-linux.yml
- LAST_FM_API_KEY/SHARED_SECRET from release-linux.yml
- Complete 'Inject API secrets' step from release-flatpak.yml

This completes the API keys refactor for Flathub compliance.
2026-01-17 16:52:34 -06:00
vicrodh 76ed4e085d Merge main into feature/api-keys-refactor
Integrate latest changes from main branch including audio configuration updates.
2026-01-17 16:51:45 -06:00
vicrodh 2495636820 Update HANDOFF document to emphasize API Keys UI cleanup requirement 2026-01-17 16:51:39 -06:00
vicrodh 9516a1c1fa Merge feature/api-keys-refactor: Remove embedded API keys and add Discogs artwork selector
This major refactor achieves Flathub compliance by removing all embedded
API credentials from the binary. All API requests now go through a
Cloudflare Workers proxy for secure credential management.

New Features:
- Discogs artwork selection UI with multi-release support
- Catalog number support for audiophile libraries
- Carousel navigation for artwork options
- Release metadata display (title, year)

Technical Changes:
- Cloudflare Workers proxy for Last.fm, Spotify, Tidal, Discogs
- Database migration adds catalog_number column
- URI scheme registration (qbz://) for OAuth flows
- Rate limiting and User-Agent validation server-side

See CHANGELOG-api-keys-refactor.md for complete details.
See POST-MERGE-CLEANUP.md for removing legacy API Keys UI.
2026-01-17 16:48:40 -06:00
vicrodh e7d7ee0938 Add .github/copilot-instructions.md to gitignore
This file contains AI agent instructions and should not be tracked.
It unifies CLAUDE.md and AGENTS.md rules for GitHub Copilot.
2026-01-17 16:35:38 -06:00