MixBoard — Knowledge Base
Accumulated findings from investigations, deliberations, and development sessions.
Updated automatically after each significant investigation.
Chad Music Integration (2026-03-13)
ADR: API Strategy — Custom REST over Subsonic (2026-03-13)
- Decision: Keep chad-music's custom REST API, no Subsonic implementation
- Rationale: MixBoard is the only client. Subsonic = 70+ endpoints for zero benefit. All three reviewer models agreed.
- Status: Active
- Key entities: ChadMusicAPIClient, /api/cat/:category, /api/album/:id/tracks
ADR: Streaming — Direct HTTP + Range Requests (2026-03-13)
- Decision: Direct HTTP file serving with Range requests (206 Partial Content). No HLS in early phases.
- Rationale: HLS breaks gapless playback on concept albums. Single user = no need for adaptive bitrate. AVPlayer handles direct HTTP natively.
- Status: Active
- Key entities: AVPlayer, AVURLAsset, AVURLAssetHTTPHeaderFieldsKey
ADR: Playback Engine — AVPlayer First (2026-03-13)
- Decision: AVPlayer for ALL playback in Phase 1 (both local file:// and cloud https:// URLs). AVAudioEngine path added in Phase 3 for cached tracks needing EQ/BPM.
- Rationale: AVPlayer handles buffering, seeking, gapless (AVQueuePlayer), and AirPlay out of the box. Dual engines add complexity.
- Status: Active
ADR: Auth — API Key via HTTP Header (2026-03-13)
- Decision: API key in macOS Keychain, sent via Authorization: Bearer header. Use AVURLAssetHTTPHeaderFieldsKey for stream URLs (undocumented but working Apple API).
- Rationale: Simplest auth for personal use. Telegram login deferred to Phase 4.
- Status: Active
- Risk: AVURLAssetHTTPHeaderFieldsKey is undocumented — may break in future macOS versions
Key Risks Identified
- Woo/Clack Range request support — MUST verify with curl before coding
- Static file auth — file-server routes may bypass with-user macro
- TLS — AVPlayer rejects self-signed certs, need Tailscale or Caddy
- OGG Vorbis — NOT supported by AVPlayer on macOS
Phase Plan
- Phase 1 (Stream & Browse): ~20-35 hours — zero backend changes needed
- Phase 2 (Upload & Sync): ~20-30 hours — new server endpoints
- Phase 3 (Native Polish): ~15-20 hours — cache, loudness, Spotlight
- Phase 4 (Social & Advanced): ~10-20 hours — Telegram, Shortcuts
Brainstorm Spec
Full spec at: Work vault → .orchestra/mixboard-chadmusic/spec.md
Phase 1 Implementation (2026-03-13) — COMPLETE
- Files created: ChadMusic.swift (models), KeychainService.swift, ChadMusicAPIClient.swift, StreamingPlayer.swift, CloudBrowserView.swift
- Files modified: PlayerViewModel.swift (dual engine routing), SettingsView.swift (Chad Music tab), SidebarView.swift (cloud section), ContentView.swift (cloud browser routing)
- Architecture: StreamingPlayer (AVPlayer) for cloud, AudioEngine (AVAudioEngine) for local — routed via isCloudPlayback flag in PlayerViewModel
- Cloud tracks are transient — ChadTrack is Codable struct, not SwiftData. No DB persistence until Phase 2 sync.
- Build note: Must run
xcodegen generate after adding new source files
- Codex review finding (fixed): URL path composition changed from
.appending(path:) to URLComponents for proper encoding
- Gemini review: False positive — applied D365 rubric to Swift project (review extension prompt template still has D365 residue for this workspace)
- Tests: 143 pass (38 new ChadMusic tests), 0 failures, no regressions
Pre-flight Verification Results (2026-03-13)
1. TLS / Network
- Server is publicly reachable — no Tailscale needed for connectivity
- URL:
https://music.chad-partners.com (confirmed via curl 2026-03-14)
- Behind Nginx 1.27.0 reverse proxy with HTTPS (HSTS enabled, max-age=31536000)
- Gogs domain:
gogs.chad-partners.com
- Default port is 5000 (from
main function: &key (port 5000)) — Nginx proxies to it
file-server is only enabled when serve-files flag is passed to main
1b. Range Request Support — VERIFIED ✅
curl -sI -H 'Range: bytes=0-1000' https://music.chad-partners.com/ → HTTP 206, Content-Range: bytes 0-1000/2998
- Nginx handles Range natively — no server-side changes needed
- AVPlayer streaming will work out of the box
2. Server Architecture (from server.lisp, verified 2026-03-14)
- Woo HTTP server (libev-based) via Clack
- Default port: 5000
- Auth: Telegram-based login flow → generates random 16-byte hex token → stored in server-side hash table
with-user macro checks Authorization: Bearer <token> header
- CLI bypass: if no
x-real-ip header AND no valid token → auto-auth as admin (for local dev/REPL)
- File serving: only active when
(main :serve-files t) is passed
- Path mapping: config.lisp maps filesystem paths to URL prefixes (e.g.
/media/music/ → /music/)
- Routes:
/api/cat/:category, /api/cat/:category/size, /api/album/:id/tracks, /api/stats, /api/rescan (POST), /api/login (POST), /api/user
3. API Response Format (from %to-json methods)
- Track JSON keys:
id, artist, album_artist, album, year, no (track number), title, bit_rate, duration, url, cover
- Album JSON keys:
id, artist, year, album, original_date, publisher, country, genre, type, status, mb_id, track_count, total_duration, cover
- Category results: array of
{"item": "name", "count": N}
- NOTE: Track number key is
no, not track_number. Album title key is album, not title. Must update ChadMusic.swift CodingKeys!
4. File-Server Auth — FILES ARE UNPROTECTED (when enabled)
file-server function does NOT use with-user macro
- All API routes ARE auth-protected
- Acceptable for personal server — file URLs are opaque (need API to discover them)
- Fix in Phase 2: wrap file-server in with-user
Cloud Upload — Phase 2 Implementation (2026-03-18)
ADR: Upload Protocol — Raw Body PUT (2026-03-18)
- Decision: Raw body PUT with X-Filename header (not multipart form upload)
- Rationale: Simpler server-side, matches existing chad-music pattern. URLSession upload task handles progress natively.
- Status: Active
ADR: Upload Storage — Atomic Write Pattern (2026-03-18)
- Decision: Write to temp file (.tmp suffix), atomic rename on success, delete on error
- Rationale: Codex + Gemini flagged partial file corruption risk on interrupted uploads
- Status: Active
- Key entities: upload-file (server.lisp), UploadService.swift
Key Technical Findings
UTType.flac does NOT exist in Xcode 16 / macOS 14 SDK — use UTType(filenameExtension: "flac")
- Woo/Clack
:raw-body is a binary stream — safe for chunked reads
get-universal-time has 1-second resolution — use timestamp + random suffix for collision avoidance
- Content-Type headers may include parameters (e.g.,
audio/mpeg; charset=binary) — strip before matching
*rescan-lock* is mutex for DB rescans — upload endpoint must acquire it
Files Created/Modified
- Created:
Sources/Services/UploadService.swift
- Modified:
Sources/Views/CloudBrowserView.swift — upload button, progress, success/error
- Artifacts:
.orchestra/cloud-upload-v1/server-patch.lisp, nginx-and-cron.conf
- Server PR:
feature/upload-endpoint branch on chad-music — merged & deployed by Enikesha (2026-03-18)
Offline Download v1 — Implementation (2026-03-18)
ADR: Download Storage — Application Support (2026-03-18)
- Decision: Store persistent downloads in
~/Library/Application Support/MixBoard/CloudTracks/{cloudTrackId}.{ext}
- Rationale: All 3 reviewers agreed ~/Library/Caches/ is wrong for user-initiated downloads (macOS can purge). Application Support survives OS cleanup and Time Machine backup.
- Status: Active
- Key entities: DownloadService.persistentStorageDirectory, Track.localCachePath
ADR: Separate localCachePath from filePath (2026-03-18)
- Decision: New
localCachePath: String? field on Track model, separate from filePath. hasLocalFile unchanged.
- Rationale:
hasLocalFile is used by export pipeline and many other consumers. Changing it would break existing functionality. New hasPlayableLocalFile computed property checks both paths.
- Status: Active
- Key entities: Track.localCachePath, Track.hasPlayableLocalFile, Track.playableFileURL
ADR: Playback Routing — hasPlayableLocalFile First (2026-03-18)
- Decision: Check
track.hasPlayableLocalFile first → AudioEngine. Fallback to StreamingPlayer if AudioEngine fails (format incompatibility). Only stream if no local file.
- Rationale: Downloaded cloud tracks should play via AudioEngine for EQ/BPM/waveform. OGG format can't be opened by AVAudioFile but stb_vorbis decoder handles it via AudioEngine's OGG path.
- Status: Active
- Key entities: PlayerViewModel.loadAndPlay, PlayerViewModel.loadAndPlayDirect, PlayerViewModel.playViaStreamingPlayer
Files Created/Modified
- Created:
Sources/Services/DownloadManager.swift — observable singleton for tracking active downloads
- Created:
Sources/Views/DownloadIndicator.swift — four-state inline download button (20pt)
- Created:
Sources/Views/AlbumDownloadButton.swift — album header aggregate download button
- Created:
Sources/Views/PlaylistDownloadButton.swift — playlist header download count button
- Modified:
Sources/Models/Track.swift — added DownloadState enum, localCachePath, downloadStateRaw, hasPlayableLocalFile, playableFileURL
- Modified:
Sources/Services/DownloadService.swift — added downloadPersistent(), removeDownload(), persistentStorageDirectory
- Modified:
Sources/Services/AudioEngine.swift — added optional fileURL parameter to loadTrack()
- Modified:
Sources/ViewModels/PlayerViewModel.swift — routing change + playViaStreamingPlayer helper
- Modified:
Sources/Views/CloudBrowserView.swift — download indicator in CloudTrackRow, album download button, download context menus, persisted track lookup
- Modified:
Sources/Views/TrackRow.swift — downloaded cloud tracks show green arrow.down.circle.fill instead of cloud.fill
- Modified:
Sources/Views/PlaylistView.swift — download context menus for cloud tracks, PlaylistDownloadButton in header
- Tests: 172 pass (1 pre-existing failure in testAllFormatsExport — unrelated E2E test), 0 new failures