AnalyticsService.track() is fire-and-forget — posts to POST /v1/marks/events
which records a Langfuse trace. No external SDK, no Sentry on iOS side.
Events tracked:
- podcast.started (url, cached)
- podcast.completed (url, durationSec) — fires at 97% playback
- podcast.failed (url, error)
- search.semantic (query, resultCount)
- enrich.completed (count)
- collections.generated (collectionCount)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MiniPlayerView: persistent bar above tab bar, shows title/progress,
play/pause and close buttons, slides in when audio starts
- PodcastPlayerViewModel lifted to BookmarksViewModel so it persists
across sheet dismissals — audio keeps playing in background
- Headphone button on every bookmark row (filled = cached, outline = not)
- start() is idempotent: re-tapping same article resumes without restart
- stop() saves playback position before teardown (PodcastProgress)
- setupPlayer restores saved position on next open
- BrowserView keeps its own local VM (stopOnDismiss: true)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PodcastIndex.swift stores {articleUrl, filename, title, createdAt} in
App Support/podcasts/index.json. Updated on every successful download.
Enables building a podcast library view later.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cache generated MP3 to App Support/podcasts/<hash>.mp3 — re-opening
the same article plays instantly with zero backend calls
- AVAudioSession .playback/.spokenAudio + .allowBluetoothA2DP — routes
audio to AirPods and Bluetooth headsets
- MPRemoteCommandCenter play/pause/seek — AirPods controls + lock screen
- MPNowPlayingInfoCenter — title and scrubber on lock screen
- UIBackgroundModes: audio in Info.plist — playback continues when backgrounded
- Auto-play when player is ready
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>