Waves 1-2: scrobble_live/index, scrobbled_tracks_live/index, release_group_show, release_show. Dropped unwrap/2 bridges and page_title/1 usage. Search form tests use visit with query params. page_title tests use assert_has("title", ...).
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.
Replace phx-click server event with Fluxon.open_dialog for zero-latency
modal opening. The modal DOM is now always present (no :if conditional) so
it can be shown instantly; the server syncs state in the background.
Fix test selectors that now match the always-present modal form by scoping
to forms without phx-target (which the modal form always has).
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.
- show/2: use case instead of bare Integer.parse match so
/api/v1/errors/not-an-id returns 404 instead of crashing
- index/2: clamp limit to >= 1 and offset to >= 0 to prevent
SQLite from treating negative values as unlimited
- Add 3 tests: non-integer id 404, negative limit/offset clamping
- 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
Map.has_key? verified the key existed but not the content. The
fixture is deterministic — assert the stored value equals
Wikipedia.Fixtures.article_extract_html/0.
Refs #176