Mirrors flutter_client/lib/cache/db.dart's CachedHomeIndex — per-item
rows that drive the Home screen sections (Recently Added Albums,
Rediscover Albums/Artists, Most Played Tracks, Last Played Artists).
Composite PK (section, position) — exactly one row per slot per
section; sync replaces in-place via upsert. entityType ("album" /
"artist" / "track") dispatches per-tile hydration to the right
per-entity endpoint when the Home screen renders.
DAO surface fits the sync flow:
- observeBySection (Flow) for the Home composables
- getBySection (suspend) for one-shot sync reads
- upsertAll for sync writes
- deleteBySection for replace-all on a section sync
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related fixes from re-reading the Drift source for slice 8:
1. The plan called this slice "last_played" but the Drift table is
`cached_resume_state` — kept Drift's name for cross-reference
during the port. Single-row JSON-blob pattern (queue, currentIndex,
positionMs, source) — ResumeController (Phase 6.5) handles the
Kotlin-side encode/decode so the schema stays stable across
resume-shape evolution.
2. CacheSource enum was incomplete: Task 4.1 ported only 3 of the 5
Drift variants. Added AUTO_LIKED + AUTO_PLAYLIST (used by the
auto-cache prefetcher to tag cached files by reason — drives the
bucket eviction priority order INCIDENTAL > AUTO_PREFETCH >
AUTO_PLAYLIST > AUTO_LIKED > MANUAL).
No data migration needed — schema version is still 1 and we have no
real users yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors flutter_client/lib/cache/db.dart's CachedMutations — the
offline-write queue that MutationQueue.enqueue() inserts into when a
server-write fails with IOException and MutationReplayer.drain() pops
from when connectivity returns (Phase 12.2).
`kind` is a string registered in `MutationKind` (Phase 12.2) so the
replayer can map to the right handler. `payload` is JSON-serialized
args. Unknown kinds get dropped at drain time rather than wedging.
DAO surface tailored to the replayer:
- observePendingCount: Flow<Int> for the offline-indicator badge
- getAll: FIFO list for drain (id ASC = oldest first)
- insert: returns the autoGenerate'd id
- recordAttempt(id, instant): atomic increment + lastAttemptAt set
- delete(id) / clear
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors flutter_client/lib/cache/db.dart's AudioCacheIndex — one row
per fully-downloaded audio file. Drives the 2-bucket LRU eviction
policy that Phase 12's AudioCacheEvictionWorker will execute.
DAO surface tailored to the eviction worker:
- totalBytes / bytesBySource — sum-of-sizeBytes for cap checks
- evictionCandidates(source) — oldest lastPlayedAt within a source
bucket, NULL lastPlayedAt sorted first (never-played candidates
evict before played ones)
- touchLastPlayed(trackId, instant) — single-column update from
the player on every "ready+playing" transition
- bulk delete by track-id list for batch evictions
CacheSource enum (MANUAL / INCIDENTAL / AUTO_PREFETCH) ported in
Task 4.1's TypeConverters.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors flutter_client/lib/cache/db.dart's CachedQuarantineMine —
the user's flagged-as-hidden tracks. Server returns the full
denormalized snapshot on /api/me/quarantine; the cache mirrors that
shape so the Quarantine screen renders without a join.
`createdAt` is a server ISO-8601 string (canonical timestamp);
`fetchedAt` is our local sync marker.
DAO covers the three known consumers:
- observeAll for the Quarantine screen (newest first)
- observeFlaggedTrackIds for feed-level filtering
- observeIsHidden(trackId) scalar Flow for per-tile UI
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related entities mirroring flutter_client/lib/cache/db.dart:
- CachedPlaylists — one row per playlist; `systemVariant` is null for
user playlists and "for_you" / "songs_like_artist" / etc. for
system-generated mixes (used by the add-to-playlist sheet filter)
- CachedPlaylistTracks — composite-PK join table, `position` carries
ordering inside a playlist
DAO surfaces split user vs system playlists at the query layer so
ViewModels don't have to filter — observeUserPlaylists/observeSystemPlaylists.
PlaylistTrackDao gets a deleteByPlaylist for the replace-all pattern
after a sync delta lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors flutter_client/lib/cache/db.dart's CachedLikes Drift table —
composite PK (userId, entityType, entityId) so the same user can
like a track and its album and its artist independently. entityType
is a plain string ("track" | "album" | "artist") for parity with the
Drift schema and the server wire format.
DAO surface tailored to consumers we know are coming:
- observeLikedTrackIds(userId): Flow<List<String>> — audio-cache
eviction reads this set to identify "liked" bucket members
- observeLikedIdsOfType(userId, entityType): generalized variant
- observeIsLiked(...): scalar Flow for LikeButton composables
- upsertAll (sync writes)
- delete (mutation queue → toggle off)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors flutter_client/lib/cache/db.dart's CachedArtists / CachedAlbums /
CachedTracks Drift tables. Library cache foundation — LibraryRepository
(Phase 5.2) reads cache-first through these DAOs and refreshes from
server via the sync controller (Phase 12.4).
Column names follow Kotlin idiom (camelCase) instead of Drift's
snake_case; the schema is internal to the native client and the wire
JSON conversion happens in feature-level mappers.
Each DAO carries:
- observe* (Flow) for cache-first reads in ViewModels
- getById/getByIds (suspend) for one-shot lookups
- upsertAll (suspend, REPLACE) for sync writes
- deleteByIds (suspend) for sync-driven deletes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M8 phase 3.1. Wires the single shared OkHttp + Retrofit instance the
whole app uses (per-endpoint Retrofit interfaces land in feature
modules). Audio HTTP via ExoPlayer's OkHttpDataSource.Factory will
reuse this same client — single auth/connection-pool surface.
- AuthStore: in-memory MutableStateFlow placeholder. Task 4.2
promotes it to a Room-backed single-row table for process-death
persistence; public API stays identical.
- AuthCookieInterceptor: attaches Cookie on outbound, captures
Set-Cookie on successful responses (login flow), clears the store
on 401 (logout signal).
- ServerBaseUrl: value class to type-safely DI the base URL.
- NetworkModule: Hilt-provided OkHttp + Retrofit + HttpLogging.
First task with unit tests — AuthCookieInterceptorTest uses MockWebServer
to verify all three interceptor branches plus a no-Set-Cookie-no-overwrite
case. Will tell us if the JUnit 5 + okhttp-mockwebserver test stack is
plumbed correctly through Gradle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Seven findings from the first real detekt run:
- LocalActionColors.kt / Tokens.kt: MatchingDeclarationName flagged
that the file names don't match the single top-level declaration.
Renamed to ActionColors.kt and FabledSwordTokens.kt (`git mv`).
- Typography.kt: three Font(...) calls exceeded the default 120-char
line length. Wrapped each named-arg list onto its own line.
- MainActivity.kt + MinstrelTheme.kt: FunctionNaming flagged App() /
MinstrelTheme() for not starting lowercase — these are
@Composable functions and PascalCase is the Compose convention.
Added a config override to the detekt YAML:
naming:
FunctionNaming:
ignoreAnnotated: ['Composable']
Matches every mainstream Compose codebase.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M8 phase 1.4. Mirrors flutter_client/lib/theme/. Source of truth for hex
values is flutter_client/shared/fabledsword.tokens.json (manual sync until
cross-language codegen lands; ports the dark-surface + flat cohort).
Material 3 ColorScheme takes accent as primary; action colors
(Moss/Bronze/Oxblood) live in LocalActionColors as semantic roles per the
project_design_system rule "NEVER use accent for action buttons".
Typography uses androidx.compose.ui.text.googlefonts to fetch Fraunces /
Inter / JetBrains Mono at runtime via Play Services Fonts — matches the
Flutter client's `google_fonts` package (no bundled .ttf files in either
tree). Weights restricted to 400/500. Fraunces is reserved for ≥18sp
display/headline slots per the design-system rule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M8 phase 1.3. Plants the Hilt entrypoint so the rest of the modules
(NetworkModule, DatabaseModule, PlayerModule) can land in subsequent
phases. WorkerFactory wired so HiltWorker can be used directly later.
Restores @AndroidEntryPoint on MainActivity (deferred from 1.2 since
Hilt KSP errors without an annotated Application class).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M8 phase 1.2. AndroidManifest declares FGS mediaPlayback +
POST_NOTIFICATIONS permissions ahead of the player phase. Activity
hosts a single Compose Scaffold for now; nav graph lands in phase 5.
Launcher icons reused from flutter_client/ (same applicationId means
same brand at cutover). MinstrelApplication referenced in manifest
but the class itself lands in Task 1.3 — manifest class names are
resolved at install time, not build time, so the intermediate commit
still builds.
@AndroidEntryPoint deferred to Task 1.3 alongside @HiltAndroidApp on
MinstrelApplication (Hilt KSP errors without an annotated Application).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>