This file tracks changes made during Copilot-assisted development sessions. Future sessions: read this first for recent context.
Problem: The app's SwiftData persistent store failed to load on launch. The entire ModelContext was broken — no playlists could be created, no data could be saved. The error:
CoreData error: entity=Playlist, attribute=groupTemplate,
reason=Validation error missing attribute values on mandatory destination attribute
Root cause: Playlist.groupTemplate (and other @Model properties across all models) were declared without default values at the declaration site. SwiftData's lightweight migration couldn't fill in missing attributes for existing records in ~/Library/Application Support/default.store.
Fix (4 files):
Sources/Models/Playlist.swift — Added defaults to all stored properties on Playlist and PlaylistEntrySources/Models/Track.swift — Added defaults to all stored propertiesSources/Models/CuePoint.swift — Added defaults (note: enum defaults must be fully qualified, e.g. CuePointType.marker not .marker, due to @Model macro)Sources/Models/PlaylistFolder.swift — Added defaults to all stored propertiesBonus fix: NewPlaylistSheet in Sources/Views/ContentView.swift now takes a @Binding var selectedPlaylist: Playlist? so newly created playlists auto-select in the sidebar.
Tests: 104 passed, 0 failures.
Key lesson: When adding new stored properties to @Model classes, always provide a declaration-site default value. SwiftData lightweight migration requires this to populate existing rows.
Change: Added .onKeyPress(.delete) handler to PlaylistEntryList in Sources/Views/PlaylistView.swift. Pressing Backspace/Delete now removes all selected entries from the current playlist, matching the existing "Remove" button and context menu behavior.
Files changed:
Sources/Views/PlaylistView.swift — Added removeSelectedEntries() method and .onDeleteCommand on the ListTests: 104 passed, 0 failures.
Problem: When adding a folder to a playlist, expandDirectories() sorted files by filename only (lastPathComponent), which interleaved files from different subfolders alphabetically. For example, tracks from Disc 1/01.mp3 and Disc 2/01.mp3 would be shuffled together.
Fix: Changed sort in expandDirectories() in Sources/Views/PlaylistView.swift to sort by full path with numeric + case-insensitive comparison (matching the iOS FolderBrowserView.allTracksRecursive behavior). Now Disc 1/01.mp3 through Disc 1/12.mp3 come before Disc 2/01.mp3.
Tests: 104 passed, 0 failures.
Problem: The {Year} grouping tag used track.dateAdded (when the file was imported), not the album's release year. iOS uses track.year from metadata.
Changes (6 files):
Sources/Models/Track.swift — Added var year: Int? (release year from metadata, with default for migration)Sources/Services/MetadataService.swift — 3-tier year extraction from metadata (matching iOS): commonKeyCreationDate → format-specific tags (TDRC, TYER, ©day, DATE) → fallback regex scanSources/Services/LibraryManager.swift — Sets track.year = metadata.year after importSources/Models/GroupTemplateResolver.swift — Replaced {Year} with {Date}, resolves to track.year; preset changed from "Album (Year)" to "Album (Date)". Legacy {Year} still resolves for backward compat.Sources/Models/FileNameTemplate.swift — {year} now resolves to track.year (release year) instead of track.dateAddedTests/Unit/ModelTests.swift — Replaced testAlbumYearTemplate with testAlbumDateTemplate + testAlbumDateTemplateNoYearTests: 105 passed, 0 failures (1 new test).
Change: Added ability to re-read metadata from audio files for existing tracks (to populate the new year field on tracks imported before the Date feature).
Files changed:
Sources/Services/LibraryManager.swift — Added rescanMetadata(_:) (single track) and rescanAllMetadata(tracks:) (bulk)Sources/Views/PlaylistView.swift — Added "Rescan Metadata" to track right-click context menuTests: 105 passed, 0 failures.
Problem: Existing tracks had NULL year after the {Date} feature was added. The importFile() early-returned for existing tracks without backfilling year. Also, readMetadata lacked year range validation in the commonKeyCreationDate branch, causing bogus values like "105" to be stored.
Changes:
Sources/Services/LibraryManager.swift — importFile() now backfills year on existing tracks; rescanMetadata() uses new lightweight readYear() methodSources/Services/MetadataService.swift — Added readYear(from:) (metadata-only, no AVAudioFile); added y > 1900 && y < 2100 validation to commonKeyCreationDate branchSources/Views/ContentView.swift — Auto-backfill on launch: fetches tracks with nil or invalid year, runs rescanAllMetadataResult: All 317 tracks in DB now have valid year values.
Change: Pressing Space toggles play/pause when MixBoard is active. Implemented via NSEvent.addLocalMonitorForEvents in MediaKeyHandler. Skips interception when a text field is focused.
Files changed:
Sources/Services/MediaKeyHandler.swift — Added AppKit import, keyMonitor property, local keyDown monitor for keyCode 49 (space)Change: Double-clicking a track in the playlist now plays it. Uses .onTapGesture(count: 2) before .onTapGesture(count: 1) on each row, with .contentShape(Rectangle()) for full row hit-testing.
Files changed:
Sources/Views/PlaylistView.swift — Added double-click and single-click tap gestures to groupContent() rowsChange: Added .help() tooltips to all player bar buttons (Stop, Previous, Play/Pause, Next, Shuffle, Repeat, Cursor Mode, Volume, Settings, Now Playing). Tooltip delay set to 200ms via NSInitialToolTipDelay.
Files changed:
Sources/Views/PlayerView.swift — Added help parameter to transport btn(), added .help() to volume controlSources/MixBoardApp.swift — Set NSInitialToolTipDelay to 200 on launchTests: 105 passed, 0 failures.