Move 7 non-critical queries out of synchronous mount into assign_async
and start_async tasks with skeleton loading placeholders.
Only scalar badge counters (collection count, wishlist count, scrobble
count) remain synchronous. All async sections use <.async_result> with
loading and failed slots. Scrobble activity preserves LiveView streams
via handle_async. On-this-day date changes use AsyncResult.ok/2.
Add MusicLibrary.Collection.Enrichment module with batch scrobble, artist
country, and selected release enrichment from existing tables.
Wire enrichment into all four collection API endpoints (index, latest,
random, on_this_day). New JSON fields: scrobble_count, last_listened_at,
artist_country, selected_release.
Uses 3 fixed-count batch queries regardless of page size — no N+1 risk.
No schema changes or migrations required.
Add RecordSets.empty_record_set/1 that bulk-deletes all items in a set via
a single DELETE query and returns the reloaded empty set. Add Empty button
with confirmation prompt to both index and show view dropdowns.
Import workers now broadcast :records_index_changed on
"records:index_changed" after successful import via
Records.broadcast_index_changed/0.
CollectionLive.Index and WishlistLive.Index subscribe to the
topic in mount/3 and reload their record streams on receipt,
with a live_action guard to skip reloads when the grid is
hidden behind a modal (:import, :barcode_scan).
IndexActions.handle_index_changed/1 refreshes total_entries
before reloading to keep the pagination bar accurate.
Phoenix.PubSub must be initialized (creating its internal Registry)
before Oban starts processing jobs. With one_for_one strategy,
children start sequentially; previously Oban was started first,
causing 'unknown registry: MusicLibrary.PubSub' errors when a
job called broadcast before PubSub was ready (error #3891).
Subscription management was only done in mount/3, meaning
navigating to a different record via handle_params/3 left the
old subscription active. A background update for the old record
would then overwrite the currently displayed record.
Fix: manage subscriptions in handle_params/3 (unsubscribe old,
subscribe new) via shared RecordActions.manage_subscription/2,
and guard handle_info({:update,...}) against mismatched IDs.
Always wrap FTS5 query terms in phrase double-quotes instead of
checking a hardcoded list of special characters. Characters like
|, ;, +, #, @ were missing from the list, causing FTS5 syntax
errors (production errors #1 and #1398). Phrase-quoting every term
produces identical results for normal ASCII search (verified via
SQL) while safely handling any character FTS5 chokes on.
Add regression tests for bare special characters and special
characters mixed with normal words.
Three-layer architecture:
- Logster v2 handles HTTP request and LiveView socket telemetry in logfmt format
- Custom formatter as safety net escaping any remaining embedded newlines
- Config flag (single_line_logging) gating Logster attachment to prod only
Dev environment retains multi-line format for readability.
- Escape LIKE wildcards (% and _) in search queries using fragment ESCAPE
to prevent accidental pattern expansion and potential DoS vectors
- Use Repo.aggregate for occurrence_count instead of loading all
occurrences into memory in get_error/1
- Use Repo.aggregate(:min, :inserted_at) for first_occurrence_at instead
of fragile List.last/1 that depends on query ordering
- Add MusicLibrary.ErrorsTest with 15 tests covering list_errors/1
(default, status/muted/search filters, wildcard escaping, pagination,
empty results) and get_error/1 (with/without occurrences, not_found)
Split the 450+ line Records context into focused modules:
- Records.Search — FTS5 search, SearchParser integration, genre listing
- Records.Import — MusicBrainz release/group import, release status
- Records.Enrichment — genre population, cover refresh, color extraction,
MusicBrainz data refresh
- Records.Query — shared helpers (essential_fields, order_alphabetically macro)
Records module is now a facade with defdelegate for compile-time safety.
CRUD and PubSub stay directly in Records. Tests moved alongside each
sub-module. Zero callers changed, 886 tests pass.
Closes ML-143 review findings:
- cast_id/1 uses Integer.parse instead of String.to_integer
- add_to_cart validates against server-side release_groups
- JS.push payload trimmed to {id, format} only
- Oban.insert_all result checked for partial failures
- handle_async success resets @importing? to false
- ImportFromMusicbrainzReleaseGroup gets unique constraint
- Record.parse_datetime/1 made public with safe match
- Workers delegate to Record.parse_datetime/1
- Test coverage: change_format, clear_cart, wishlist single import
SQLite interprets bare years (e.g. "1970") as Julian day numbers
in strftime, producing wrong month-day matches. Filter to only
full YYYY-MM-DD dates before comparing.