Files
yellowjacket/.planning/codebase/CONCERNS.md
T
2026-02-26 16:58:25 -05:00

21 KiB

Codebase Concerns

Analysis Date: 2026-02-26

Tech Debt

Hardcoded Speaker Configuration:

  • Issue: Speaker sample rate (44100) and buffer size (100ms) are hardcoded constants with no user configuration
  • Files: backend/player/player.go line 104, line 127
  • Impact: Users with high-resolution audio (96kHz, 192kHz) get resampled down to 44.1kHz. Users cannot tune buffer size for latency vs. stability tradeoff
  • Fix approach: Add AudioOutput section to config TOML (SampleRate, BufferSizeMs). Plumb through to InitSpeaker() and updateStreamers() resample quality param (currently hardcoded 4 at line 308)

Fixed Resample Quality:

  • Issue: Resample quality is hardcoded to 4 in beep.Resample() call
  • Files: backend/player/player.go line 307-309
  • Impact: No ability to trade CPU for quality. Low quality may produce audible artifacts on large sample rate deltas
  • Fix approach: Make resample quality configurable via config, expose in settings UI. The TODO comment at line 307 acknowledges this

Tag Writing Not Implemented:

  • Issue: Track details editing UI exists but save is a no-op
  • Files: frontend/src/components/track-details/track-details.ts line 651
  • Impact: Users see an edit interface that doesn't persist changes. Misleading UX
  • Fix approach: Implement backend tag writing endpoint using a tag library (e.g. github.com/dhowden/tag already in deps supports reading; writing may need additional library). Gate the save button behind a "tag writing supported" check

HTML Template Component Incomplete:

  • Issue: The struct2html templ component has a TODO for supporting more types
  • Files: pkg/templcomp/struct2html_templ.go line 242
  • Impact: Config page form generation may not handle all field types correctly
  • Fix approach: Extend the type switch to cover missing types (maps, nested structs, etc.)

Package-Level startupErr Variable:

  • Issue: startupErr is a package-level mutable variable used to communicate startup failures between OnStartup and OnDomReady
  • Files: backend/app.go line 134
  • Impact: Not thread-safe if Wails calls these lifecycle methods concurrently. Also makes testing difficult
  • Fix approach: Move to a field on YellowJacketApp struct, protected by the struct's lifecycle guarantees

Code Quality

Large Frontend Components:

  • Issue: Several Lit components exceed 1000+ lines, combining rendering, state management, event handling, drag-and-drop, context menus, and search filtering
  • Files:
    • frontend/src/components/playlist-view/playlist-view.ts (2669 lines)
    • frontend/src/components/cover-grid/cover-grid.ts (2092 lines)
    • frontend/src/components/track-list/track-list.ts (1875 lines)
    • frontend/src/components/config-page/config-page.ts (1464 lines)
    • frontend/src/components/queue-panel/queue-panel.ts (1424 lines)
  • Impact: Difficult to reason about, test in isolation, or modify without regressions. High coupling between rendering and business logic
  • Fix approach: Extract reusable behaviors into additional controllers (the project already uses SelectionController, ContextMenuController, etc.). Consider splitting rendering into sub-components

Large Backend Files:

  • Issue: backend/playlist/playlist.go (1778 lines) and backend/library/library.go (1328 lines) handle too many responsibilities
  • Files: backend/playlist/playlist.go, backend/library/library.go
  • Impact: Hard to navigate; mixing CRUD, M3U8 file management, phantom resolution, and search in a single file
  • Fix approach: playlist.go already has some splitting (m3u.go, match.go, favorites.go). Consider further extraction: phantom resolution into phantom.go, M3U file management is already split. Library could extract saveAudioFile/updateAudioFileMetadata/processMetadata into a dedicated import.go file

Duplicated FTS Search Query:

  • Issue: The same complex FTS5 JOIN query pattern (audio_files + recordings + artist_credit + release_group_recordings + release_groups) is repeated in SearchFTS, SearchFTSByFilename, SearchFTSTracks, RebuildSearchIndex, and migration2BasenameAndFTS
  • Files: backend/database/search.go lines 34-57, 92-116, 232-274, 168-188; backend/database/database.go lines 287-311
  • Impact: Changes to the schema require updating 5+ copies of essentially the same JOIN pattern. Risk of them diverging
  • Fix approach: Extract the common JOIN clause into a constant or query builder helper. Alternatively, consolidate into fewer sqlc-generated queries

Raw SQL in Persistence Layer:

  • Issue: Queue persistence and search use hand-crafted SQL with string concatenation for batch operations (lookupChunk, insertTrackBatch) instead of sqlc-generated queries
  • Files: backend/queue/persistence.go lines 56-73, 186-203; backend/database/search.go
  • Impact: These queries bypass sqlc's type-safety guarantees. The fmt.Sprintf pattern for IN clauses is safe (only ? placeholders are interpolated) but diverges from the project's pattern of using generated queries
  • Fix approach: Consider using sqlc's sqlc.slice() feature or a query builder for batch operations. Alternatively, document these as intentional exceptions

Error Handling Gaps

Swallowed Errors in App Lifecycle Callbacks:

  • Issue: MPRIS callbacks in app.go discard errors from Pause() and Seek() with _ =
  • Files: backend/app.go lines 183, 186, 191, 195
  • Impact: If pause or seek fails from OS media controls, the failure is invisible to the user and to logs
  • Fix approach: Log errors at minimum. Consider emitting a frontend notification for user-visible failures

Silently Swallowed Artist Credit Link Error:

  • Issue: CreateArtistCreditArtist result and error are both discarded with _, _
  • Files: backend/library/library.go line 1092
  • Impact: If the link creation fails for a non-duplicate reason, the data model is silently incomplete
  • Fix approach: Check error; ignore only UNIQUE constraint violations (which are expected for idempotent upserts), log all others

Library Scan Error Accumulation:

  • Issue: Scan() accumulates errors via errors.Join but individual file failures don't stop the scan — which is correct behavior — but the accumulated scanErr is returned alongside valid metrics, and callers may not distinguish "scan completed with warnings" from "scan failed"
  • Files: backend/library/library.go lines 216-218, 310-320, 427-430
  • Impact: Callers cannot differentiate between partial success and total failure
  • Fix approach: Consider separating scan warnings from fatal scan errors. Return warnings in metrics, fatal errors as the error return

Config File Permissions:

  • Issue: Config file is written with 0o666 permissions
  • Files: backend/config/config.go line 152
  • Impact: On multi-user systems, any user can read/write the config file. While this is a desktop app, it's not best practice
  • Fix approach: Use 0o644 or 0o600 for user-only read/write

Performance Concerns

Eager Full-Library Fetch on Startup:

  • Issue: libraryStore.eagerFetch() calls GetAllTracks(), GetAllAlbums(), GetAllArtists(), GetAllGenres() simultaneously on construction
  • Files: frontend/src/store/library-store.ts lines 300-305
  • Impact: For large libraries (50k+ tracks), this loads all track data into memory at once. Each call triggers a full table scan with multiple JOINs
  • Fix approach: Consider lazy loading only the active view's data, or implement pagination. The GetAllTracks query with full metadata joins is particularly expensive for large libraries

Full Queue Re-persist on Every Mutation:

  • Issue: commitMutation() calls persistTracks() which does DELETE FROM queue_tracks + batch INSERT for the entire queue on every add/remove/move operation
  • Files: backend/queue/persistence.go lines 118-178; backend/queue/queue.go line 1157
  • Impact: For a queue with thousands of tracks, every single track add/remove triggers a full table rewrite. This is O(n) for every mutation
  • Fix approach: Use incremental persistence (INSERT/DELETE individual rows) for add/remove operations. Reserve full rewrite for SetQueue and restore

SetQueue Phase 2 Re-lookups All Tracks:

  • Issue: resolveRemainingTracks re-fetches metadata for ALL file paths including those already resolved in Phase 1
  • Files: backend/queue/queue.go lines 258-311
  • Impact: For large albums/playlists, this doubles the DB work for the initial batch
  • Fix approach: Pass the already-resolved metadata from Phase 1 to Phase 2, only lookup the remaining paths

Entity Cache Never Evicted During Scan:

  • Issue: The entityCache in library scanning grows unbounded during a scan - it accumulates every artist, album, genre, and cover art seen
  • Files: backend/library/library.go lines 41-61
  • Impact: For very large libraries with thousands of unique artists/albums, this could consume significant memory. However, since it's only held for the duration of a scan and reduces DB round-trips, this is an acceptable tradeoff for most libraries
  • Fix approach: Low priority. Could add an LRU eviction policy if memory becomes an issue with extremely large libraries

Security Considerations

File Path Handling:

  • Risk: Library scan uses filepath.Join(basePath, path) where path comes from fs.WalkDir which should be safe, but playlist import accepts user-provided file paths (ImportPlaylist, AddTracksToPlaylist)
  • Files: backend/playlist/playlist.go lines 677-784, 442-484; backend/library/library.go line 247
  • Current mitigation: File paths come from Wails file dialogs (OS-level) and are validated by checking file existence. sqlc parameterized queries prevent SQL injection
  • Recommendations: Consider adding path traversal validation (ensure paths don't escape expected directories). Validate that playlist import paths resolve within the library directory

SQL Injection Protection:

  • Risk: Most queries use sqlc-generated parameterized queries, but hand-crafted SQL exists in search and queue persistence
  • Files: backend/queue/persistence.go lines 64-73, 195-198; backend/database/search.go lines 34-58, 92-116
  • Current mitigation: All hand-crafted queries use ? placeholders with separate args — no string interpolation of user values into SQL
  • Recommendations: The fmt.Sprintf in lookupChunk only interpolates placeholder strings ("?" literals), not user data. This is safe but should be documented with a comment explaining why

Config Data Logged:

  • Risk: Config struct is attached to the logger context at construction time
  • Files: backend/config/config.go line 46
  • Current mitigation: Config currently contains no secrets (file paths, theme settings, window dimensions)
  • Recommendations: If secrets are ever added to config (API keys, auth tokens), the logger attachment must be removed or filtered

Fragile Areas

Event Name Synchronization:

  • Files: backend/events/events.go, frontend/src/events.ts
  • Why fragile: Event names must match exactly between Go and TypeScript. There is no compile-time or runtime verification that they match. A typo in either file silently breaks communication
  • Safe modification: Always update both files simultaneously. The AGENTS.md documents this requirement
  • Test coverage: No automated test verifies event name parity

Player Lock Ordering:

  • Files: backend/player/player.go lines 31-39
  • Why fragile: The player has two locks (its own sync.Mutex and the global speaker.Lock()) with a documented ordering requirement: "always acquire p.mu BEFORE speaker.Lock()". The onPlaybackFinished callback runs on a goroutine to avoid holding both locks simultaneously
  • Safe modification: Never call speaker.Lock() while holding p.mu in a code path that could block. The go p.onPlaybackFinished() pattern in the beep callback (line 351) is critical — removing the goroutine dispatch would deadlock
  • Test coverage: No test validates the lock ordering. The integration test requires hardware

Two-Phase Queue Initialization:

  • Files: backend/queue/queue.go lines 152-251
  • Why fragile: SetQueue uses a two-phase approach with generation counters to handle concurrent calls. The background goroutine (resolveRemainingTracks) must check the generation counter under the lock to avoid overwriting newer state
  • Safe modification: Always increment setQueueGen before starting background work. Always check the counter both before and after acquiring the lock
  • Test coverage: No unit test for concurrent SetQueue calls

Player SetContext Double Lock:

  • Files: backend/player/player.go lines 163-171
  • Why fragile: SetContext acquires and releases p.mu twice in succession. Between the two lock acquisitions, another goroutine could modify state
  • Safe modification: Consider combining into a single lock acquisition, or document why the two-phase approach is intentional (it appears to be separating the context set from the state restore for clarity)
  • Test coverage: Integration test only

Config TOML Serialization Roundtrip:

  • Files: backend/config/config.go lines 100-139, 142-160
  • Why fragile: Load() applies defaults, then decodes TOML over them, then validates. If a new config field is added without a proper default, existing config files will have the zero value. The applyDefaults() runs after decode which could overwrite valid zero values
  • Safe modification: Always add defaults in applyDefaults() for new fields. Test with an empty config file

Missing Features

No Graceful Scan Cancellation:

  • Problem: Library scan cannot be cancelled by the user once started
  • Files: backend/library/library.go lines 166-540
  • Blocks: Users with large libraries cannot abort a scan that's taking too long. The l.ctx.Done() checks exist but depend on the Wails context which is only cancelled on app shutdown
  • Fix approach: Add a separate cancellation context that can be triggered from the frontend

No Database Connection Pooling/Health Check:

  • Problem: The database connection is opened once at startup with no health checking or reconnection logic
  • Files: backend/database/database.go lines 35-136
  • Blocks: If the SQLite file becomes corrupted or the disk fills up, errors propagate to every component with no recovery path
  • Fix approach: Add a health check method and consider periodic PRAGMA integrity_check for dev builds

No Cross-Platform Media Controls:

  • Problem: Media controls only work on Linux (MPRIS). macOS and Windows get a no-op stub
  • Files: backend/mediacontrols/mpris_linux.go, backend/mediacontrols/stub.go
  • Blocks: macOS users cannot control playback from the media keys overlay or Control Center
  • Fix approach: Implement NSMPRemoteCommandCenter for macOS, SystemMediaTransportControls for Windows

Test Coverage Gaps

No Queue Unit Tests:

  • What's not tested: Queue operations (SetQueue, AddTrack, RemoveTrack, Next, Previous, shuffle, repeat modes, persistence)
  • Files: backend/queue/queue.go, backend/queue/navigation.go, backend/queue/persistence.go, backend/queue/handlers.go
  • Risk: The queue is central to playback. Bugs in index tracking, shuffle order, or persistence could cause tracks to skip, repeat incorrectly, or lose the queue on restart
  • Priority: High

No Library Service Unit Tests:

  • What's not tested: Library scan logic, metadata processing, entity cache behavior, batch commit logic, orphan cleanup
  • Files: backend/library/library.go, backend/library/rescan.go, backend/library/coverart.go
  • Risk: Scan bugs could silently drop tracks, create duplicate entities, or fail to clean up orphans
  • Priority: High

No Database Layer Tests:

  • What's not tested: Search index operations (FTS5 queries), migration logic, transaction handling
  • Files: backend/database/search.go, backend/database/database.go
  • Risk: FTS5 query edge cases (special characters, empty queries, very long queries) and migration failures on existing databases
  • Priority: Medium

No Config Tests:

  • What's not tested: Config load/save roundtrip, validation, default application, migration from older config formats
  • Files: backend/config/config.go
  • Risk: Config corruption or silent loss of settings on upgrade
  • Priority: Medium

Player Tests Require Hardware:

  • What's not tested: All player tests require an audio device and are skipped in CI
  • Files: backend/player/player_test.go line 21
  • Risk: Player regressions are only caught manually. The volume conversion, streamer chain, and state persistence logic could all be tested without hardware
  • Priority: Medium — extract pure logic (volume math, state serialization) into testable functions

No Frontend Tests:

  • What's not tested: All TypeScript/Lit components, stores, and controllers
  • Files: frontend/src/ (entire directory)
  • Risk: Frontend regressions in event handling, state synchronization, search filtering, drag-and-drop, and selection logic
  • Priority: Medium — the backend is the source of truth, but frontend-only logic (search ranking, column sorting, selection controller) could have unit tests

Concurrency Concerns

Queue Context Set Without Lock:

  • Issue: Queue.SetContext() sets q.ctx without holding q.mu, while q.ctx is read by emit methods that are called under q.mu
  • Files: backend/queue/queue.go lines 134-136
  • Impact: Technically a data race on q.ctx if SetContext is called concurrently with emit methods. In practice, SetContext is called once during startup before any other queue operations
  • Fix approach: Acquire q.mu in SetContext for correctness

Library Fields Not Protected:

  • Issue: Library struct fields (ctx, conf, rescanHooks) are set via setter methods without any synchronization
  • Files: backend/library/library.go lines 78-84, 88-90, 120-123
  • Impact: If SetContext, SetRescanHooks, or config updates occur concurrently with a scan, there could be data races. In practice, these are called during the single-threaded startup phase
  • Fix approach: Low priority — document the "set during startup only" contract, or add a mutex if the initialization order becomes less predictable

Playlist Service Context Race:

  • Issue: playlist.Service has a ctx field set by SetContext() without synchronization, read by emitEvent() and all methods
  • Files: backend/playlist/playlist.go lines 98-104, 130-133, 1169-1178
  • Impact: Same pattern as Queue — safe in practice due to startup ordering but technically a race
  • Fix approach: Same as Queue — acquire lock or document contract

Frontend Concerns

No Event Listener Cleanup:

  • Issue: Singleton stores (playerStore, queueStore, libraryStore) register EventsOn listeners in their constructors but never unregister them
  • Files: frontend/src/store/player-store.ts lines 54-71, frontend/src/store/queue-store.ts lines 65-105, frontend/src/store/library-store.ts line 51
  • Impact: As singletons that live for the app lifetime, this is acceptable — they never need cleanup. However, the Wails EventsOn API returns a cancel function that is never captured. If the architecture ever changes to non-singleton stores, this would leak
  • Fix approach: Low priority — capture the cancel functions for documentation purposes even if they're never called

Library Store Potential Memory Pressure:

  • Issue: libraryStore caches the entire track, album, artist, and genre lists in memory simultaneously
  • Files: frontend/src/store/library-store.ts lines 29-32
  • Impact: For a library with 100k+ tracks, this could be tens of MB of JavaScript objects. The eager fetch on construction (eagerFetch()) means all four datasets are loaded simultaneously
  • Fix approach: Consider lazy loading per-view and releasing data for inactive views, or implementing virtual scrolling data providers that don't require holding the full dataset

Queue Store Delta Application Trusts Backend:

  • Issue: The applyTracksDelta method in QueueStore applies backend-sent delta operations without validation. If the frontend state diverges from the backend (e.g. missed event), the delta application produces incorrect state
  • Files: frontend/src/store/queue-store.ts lines 107-171
  • Impact: Could cause visual glitches where the queue panel shows incorrect tracks or indices. The full-state QueueChanged event acts as a periodic correction mechanism
  • Fix approach: Consider adding a sequence number or hash to detect state divergence and trigger a full re-sync

Dependencies at Risk

Wails v2 Framework Lock-in:

  • Risk: Wails v2 uses WebView2 (Windows), WebKit2 (Linux), WKWebView (macOS). The project requires -tags webkit2_41 for Linux builds. Wails v3 is in active development with breaking API changes
  • Impact: Migration to Wails v3 will require significant refactoring of the lifecycle management (OnStartup, OnDomReady, OnShutdown), event system, and binding registration
  • Migration plan: Monitor Wails v3 stability. The event-based architecture and clean separation of concerns make migration more feasible than a tightly coupled approach

beep Audio Library:

  • Risk: The gopxl/beep/v2 library handles all audio decoding and playback. It wraps platform-specific audio output (oto) and codec libraries. The speaker is initialized with global state (speaker.Init, speaker.Lock)
  • Impact: The global speaker lock creates an implicit coupling between all audio operations. If beep has bugs in seeking or resampling, workarounds are limited
  • Migration plan: The metadata.DecodeFile() abstraction and TrackLoader interface provide some insulation. A replacement would require reimplementing the streamer chain

Concerns audit: 2026-02-26