Run prettier on backlog folder

This commit is contained in:
Claudio Ortolina
2026-05-04 21:22:27 +01:00
parent 7bf89d9a1d
commit dc3b31378d
177 changed files with 2594 additions and 936 deletions
@@ -1,20 +1,21 @@
---
id: ML-1
title: 'Add default :limit to Maintenance orphan queries'
title: "Add default :limit to Maintenance orphan queries"
status: To Do
assignee: []
created_date: '2026-04-20 08:44'
updated_date: '2026-04-24 06:59'
created_date: "2026-04-20 08:44"
updated_date: "2026-04-24 06:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/183'
- "https://github.com/cloud8421/music_library/issues/183"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-16 · updated 2026-04-16_
## Summary
@@ -38,10 +39,13 @@ def get_artists_missing_musicbrainz_id(opts \\ []) do
# ...
end
```
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 Both functions apply a default limit when none is supplied
- [ ] #2 Existing callers continue to override when they need a different value
<!-- AC:END -->
@@ -49,6 +53,7 @@ end
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Closed as not-planned (YAGNI).
The proposed fix was to add a default `:limit` (e.g. 1000) to `get_artists_missing_musicbrainz_id/1` and `get_albums_missing_musicbrainz_id/1` in `lib/music_library/maintenance.ex` to defend against future unbounded call sites.
@@ -56,4 +61,5 @@ The proposed fix was to add a default `:limit` (e.g. 1000) to `get_artists_missi
Assessment: the dataset ceiling is <5,000 records and <1,000 artists for the next 2-3 years. An unbounded scan of the `tracks` table grouped by artist/album JSON fields is well within acceptable cost for this size. Adding a default limit would silently cap results on a dataset that should never need capping, and the only current callers (`lib/mix/tasks/scrobble/audit.ex:127,149`) already invoke without `:limit` on purpose.
A follow-up could remove the `:limit` option entirely to simplify the API — not tracked here; revisit if the option starts causing confusion.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -3,16 +3,18 @@ id: ML-123
title: Hide collected release input and details for wishlisted records
status: To Do
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/40'
- "https://github.com/cloud8421/music_library/issues/40"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-07 · updated 2025-05-08 · closed 2025-05-08 · not planned_
<!-- SECTION:DESCRIPTION:END -->
@@ -3,7 +3,7 @@ id: ML-142.1
title: Port Finished at picker and selection bar to ScrobbleLive.Show
status: To Do
assignee: []
created_date: '2026-04-22 16:30'
created_date: "2026-04-22 16:30"
labels:
- ui
- scrobble
@@ -19,6 +19,7 @@ priority: medium
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Bring the `/scrobble/:release_id` page (`MusicLibraryWeb.ScrobbleLive.Show`) up to ML-142 parity for the two affordances that affect scrobbling outcomes: the `Finished at` date/time picker and the pinned selection-bar footer.
Today the page imports `medium/1` and `scrobble_button_label/1` from `MusicLibraryWeb.Components.Release` — so the per-medium UI stays visually consistent — but reimplements its own `scrobble_release`, `scrobble_medium`, and `scrobble_selected_tracks` handlers, each hard-coding `DateTime.utc_now/0`. There is no picker (users cannot back-date or forward-date a scrobble) and no selection bar (when tracks are selected, the user sees no count/duration summary and the only scrobble affordance is an icon-only header button whose behaviour depends on hidden selection state via `scrobble_button_label/1`).
@@ -42,7 +43,9 @@ Today the page imports `medium/1` and `scrobble_button_label/1` from `MusicLibra
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 The page renders a `Finished at` date/time picker in the tracks section header when Last.fm is connected, initialised to the current time at mount
- [ ] #2 The picker has a `Now` reset affordance that resets the value back to the current time
- [ ] #3 `Scrobble release` uses the picker value; no longer calls `DateTime.utc_now/0` directly inside the handler
@@ -3,26 +3,31 @@ id: ML-159
title: Replicate CI pipeline on tangled.org
status: Done
assignee: []
created_date: '2026-05-03 16:54'
updated_date: '2026-05-03 19:46'
created_date: "2026-05-03 16:54"
updated_date: "2026-05-03 19:46"
labels: []
dependencies: []
documentation:
- 'doc-4 - CI Pipeline Porting Analysis: GitHub Actions → Tangled Spindles'
- "doc-4 - CI Pipeline Porting Analysis: GitHub Actions → Tangled Spindles"
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Port the entire GitHub CI pipeline (lint, test, deploy) to tangled.org. The current pipeline includes: linting (format, gettext, credo, sobelow, mix_audit, shellcheck, Docker image validation, asset build), testing (partitioned tests with coverage ≥75%), deployment (Coolify API trigger via hurl, health check, post-deploy verification), dependency submission reporting, and a manual verification workflow. The goal is to replicate this functionality on tangled.org as the CI provider.
<!-- SECTION:DESCRIPTION:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Cancelled. Two reasons:
1. **Dev/CI parity loss** — moving from mise (used in both local dev and GitHub Actions) to Nix-based tooling would create a divergence between how developers install tools and how CI does. Mise reads `mise.toml` directly; Spindle would require either mise-as-nixpkgs-dep (adding download latency) or custom Nix derivations (adding maintenance burden).
2. **No build artifact caching** — Spindle has no `actions/cache` equivalent. `mix deps`, `_build`, `node_modules`, and asset builds would recompile from scratch every run. With Elixir 1.20.0-rc.4 not in nixpkgs, even the toolchain download is uncached. Cold runs estimate 10-20 min vs ~5 min cached on GitHub Actions.
Future direction: evaluate Buildkite (free solo tier, can wire production server as runner) or similar provider that supports caching and keeps mise-based tooling.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -3,17 +3,18 @@ id: ML-16
title: Inconsistent HTTP timeout configuration across API clients
status: To Do
assignee: []
created_date: '2026-04-20 08:50'
created_date: "2026-04-20 08:50"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/163'
- "https://github.com/cloud8421/music_library/issues/163"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-09 · closed 2026-04-08 · not planned_
## Summary
@@ -40,7 +41,9 @@ HTTP timeout configuration varies significantly across API integrations with no
3. Consider making timeouts configurable via each API's Config module
## Acceptance Criteria
<!-- AC:BEGIN -->
- All API clients have explicit, intentional timeout configuration
- Timeout choices are documented (e.g., in Config module comments)
<!-- SECTION:DESCRIPTION:END -->
@@ -3,22 +3,24 @@ id: ML-18
title: Three representations of artists without clear canonical source
status: To Do
assignee: []
created_date: '2026-04-20 08:50'
created_date: "2026-04-20 08:50"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/161'
- "https://github.com/cloud8421/music_library/issues/161"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-09 · closed 2026-04-08 · not planned_
## Summary
Artist data exists in three different forms with no clear canonical source:
1. `Record` embeds `:artists` (embedded schema in record)
2. `ArtistRecord` — separate lookup table/view
3. `ArtistInfo` — external metadata store
@@ -40,7 +42,9 @@ Artist data exists in three different forms with no clear canonical source:
Document the purpose and canonical use case for each representation. Consider whether `ArtistRecord` (DB view) can be simplified or whether the embedded artists on Record should be the single source for record-artist relationships.
## Acceptance Criteria
<!-- AC:BEGIN -->
- Clear documentation of each representation's purpose
- Reduced confusion about which to use in new features
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-19
title: Circular dependency between Records and Collection/Wishlist
status: To Do
assignee: []
created_date: '2026-04-20 08:50'
created_date: "2026-04-20 08:50"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/160'
- "https://github.com/cloud8421/music_library/issues/160"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-09 · closed 2026-04-06 · not planned_
## Summary
@@ -37,7 +38,9 @@ _GitHub: created 2026-04-05 · updated 2026-04-09 · closed 2026-04-06 · not pl
Make Collection and Wishlist sub-modules of Records (`Records.Collection`, `Records.Wishlist`) or simple functions within Records that accept a filter parameter.
## Acceptance Criteria
<!-- AC:BEGIN -->
- No circular dependency between context modules
- Collection/Wishlist filtering remains functional
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-20
title: Missing indexes on JSON extraction queries in ListeningStats
status: To Do
assignee: []
created_date: '2026-04-20 08:50'
created_date: "2026-04-20 08:50"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/159'
- "https://github.com/cloud8421/music_library/issues/159"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-12 · closed 2026-04-12 · not planned_
## Summary
@@ -38,7 +39,9 @@ CREATE INDEX idx_tracks_album_mbid ON tracks(json_extract(album, '$.musicbrainz_
Profile queries before and after to confirm impact.
## Acceptance Criteria
<!-- AC:BEGIN -->
- Expression-based indexes exist for hot JSON extraction paths
- Query plans show index usage for filtered ListeningStats queries
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-23
title: StatsLive.Index runs 4+ sequential queries in mount
status: To Do
assignee: []
created_date: '2026-04-20 08:51'
created_date: "2026-04-20 08:51"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/156'
- "https://github.com/cloud8421/music_library/issues/156"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-09 · closed 2026-04-09 · not planned_
## Summary
@@ -35,7 +36,9 @@ _GitHub: created 2026-04-05 · updated 2026-04-09 · closed 2026-04-09 · not pl
Use `start_async` or `assign_async` to load stats data concurrently after mount, showing placeholder/loading states for each section.
## Acceptance Criteria
<!-- AC:BEGIN -->
- Stats page initial render is faster
- Data loads concurrently where possible
- Loading states are shown while data is fetched
@@ -3,17 +3,18 @@ id: ML-26
title: No retry/backoff strategy for non-Last.fm APIs
status: To Do
assignee: []
created_date: '2026-04-20 08:51'
created_date: "2026-04-20 08:51"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/153'
- "https://github.com/cloud8421/music_library/issues/153"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-09 · closed 2026-04-08 · not planned_
## Summary
@@ -39,7 +40,9 @@ Only the Last.fm integration has structured error responses with retry classific
3. Classify rate-limit responses separately from transient errors
## Acceptance Criteria
<!-- AC:BEGIN -->
- API-specific workers handle rate-limit responses with appropriate snooze durations
- Transient errors use exponential backoff rather than immediate retry
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-27
title: 11 copy-pasted Oban workers for refresh operations
status: To Do
assignee: []
created_date: '2026-04-20 08:51'
created_date: "2026-04-20 08:51"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/152'
- "https://github.com/cloud8421/music_library/issues/152"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-12 · closed 2026-04-12 · not planned_
## Summary
@@ -31,7 +32,9 @@ Batch (5): `ArtistRefreshAllDiscogsData`, `ArtistRefreshAllMusicBrainzData`, `Ar
Consider a generic worker pattern or macro that accepts the delegated function as a parameter, reducing boilerplate while keeping each worker as a distinct module for queue routing.
## Acceptance Criteria
<!-- AC:BEGIN -->
- Boilerplate is reduced
- Each worker remains individually identifiable for Oban queue routing and monitoring
- No change in worker behaviour
@@ -3,17 +3,18 @@ id: ML-28
title: Records context has 8+ responsibilities
status: To Do
assignee: []
created_date: '2026-04-20 08:51'
created_date: "2026-04-20 08:51"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/151'
- "https://github.com/cloud8421/music_library/issues/151"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-11 · closed 2026-04-11 · not planned_
## Summary
@@ -33,6 +34,7 @@ The `Records` context module handles search, genre management, cover art operati
## Suggested Fix
Extract into focused sub-modules:
- `Records.Search` — search operations
- `Records.Metadata` — MusicBrainz sync, genre population
- `Records.Assets` — covers, colors
@@ -41,7 +43,9 @@ Extract into focused sub-modules:
Keep the `Records` module as the public API that delegates to sub-modules.
## Acceptance Criteria
<!-- AC:BEGIN -->
- Each sub-module has a single clear responsibility
- Public API remains unchanged for callers
- No regression in functionality
@@ -3,18 +3,19 @@ id: ML-3
title: Document intentional async-only coupling from Records to Artists
status: To Do
assignee: []
created_date: '2026-04-20 08:44'
updated_date: '2026-04-20 08:44'
created_date: "2026-04-20 08:44"
updated_date: "2026-04-20 08:44"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/181'
- "https://github.com/cloud8421/music_library/issues/181"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-16 · updated 2026-04-19 · closed 2026-04-19 · not planned_
## Summary
@@ -37,10 +38,13 @@ Add a short inline comment above each call site explaining:
# Records.create/delete -> Artists.refresh/prune (sync) -> Records (embedding regeneration)
Artists.refresh_artist_info_async(artist)
```
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 Inline comments added at records.ex:385 and :415
- [ ] #2 Comments explain the runtime-cycle motivation, not just "it's async"
<!-- AC:END -->
@@ -3,29 +3,30 @@ id: ML-66
title: Large modules unchanged since last audit
status: To Do
assignee: []
created_date: '2026-04-20 08:55'
created_date: "2026-04-20 08:55"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/109'
- "https://github.com/cloud8421/music_library/issues/109"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-03-12 · updated 2026-04-09 · closed 2026-03-16 · not planned_
## Description
The four modules flagged in #99 remain at the same size:
| File | LOC |
|------|-----|
| `lib/music_library_web/live/artist_live/show.ex` | 804 |
| File | LOC |
| ------------------------------------------------------- | --- |
| `lib/music_library_web/live/artist_live/show.ex` | 804 |
| `lib/music_library_web/components/record_components.ex` | 761 |
| `lib/music_library_web/components/record_form.ex` | 652 |
| `lib/music_brainz/api.ex` | 564 |
| `lib/music_library_web/components/record_form.ex` | 652 |
| `lib/music_brainz/api.ex` | 564 |
Not urgent — only refactor when new feature work naturally touches these files.
@@ -36,4 +37,5 @@ Monitor and consider extraction when adding new features to these modules.
## Source
From technical debt audit (2026-03-12). Continuation of #99.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-68
title: Random data in test fixtures
status: To Do
assignee: []
created_date: '2026-04-20 08:55'
created_date: "2026-04-20 08:55"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/107'
- "https://github.com/cloud8421/music_library/issues/107"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-03-12 · updated 2026-04-09 · closed 2026-03-16 · not planned_
## Description
@@ -36,4 +37,5 @@ Use deterministic sequences (e.g., based on `System.unique_integer`) or seeded r
## Source
From technical debt audit (2026-03-12). Residual from #85.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-69
title: ArgumentError raises in ScrobbleActivity
status: To Do
assignee: []
created_date: '2026-04-20 08:55'
created_date: "2026-04-20 08:55"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/106'
- "https://github.com/cloud8421/music_library/issues/106"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-03-12 · updated 2026-04-09 · closed 2026-03-14 · not planned_
## Description
@@ -27,4 +28,5 @@ Return `{:error, :invalid_options}` (or similar) and handle upstream with a user
## Source
From technical debt audit (2026-03-12).
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-70
title: Bare catch blocks in telemetry storage
status: To Do
assignee: []
created_date: '2026-04-20 08:55'
created_date: "2026-04-20 08:55"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/105'
- "https://github.com/cloud8421/music_library/issues/105"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-03-12 · updated 2026-04-09 · closed 2026-03-16 · not planned_
## Description
@@ -27,4 +28,5 @@ Add `Logger.debug` calls inside the catch blocks to aid troubleshooting ETS tabl
## Source
From technical debt audit (2026-03-12).
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-76
title: Large modules approaching refactoring threshold
status: To Do
assignee: []
created_date: '2026-04-20 08:57'
created_date: "2026-04-20 08:57"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/99'
- "https://github.com/cloud8421/music_library/issues/99"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-03-05 · updated 2026-03-06 · closed 2026-03-06 · not planned_
## Priority: Low
@@ -36,4 +37,5 @@ Monitor these modules and consider extraction when adding new features touches t
## Source
From technical debt audit (2026-03-05), updated 2026-03-06.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-89
title: Random data in test fixtures
status: To Do
assignee: []
created_date: '2026-04-20 08:57'
created_date: "2026-04-20 08:57"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/85'
- "https://github.com/cloud8421/music_library/issues/85"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-02-17 · updated 2026-03-06 · closed 2026-03-06 · not planned_
## Priority: Low
@@ -29,4 +30,5 @@ Consider using deterministic sequences or seeded randomness for reproducible tes
## Source
From technical debt audit (2026-02-17), item #12.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,12 +3,12 @@ id: ML-10
title: Break Records → Artists → Collection → Records static alias cycle
status: Done
assignee: []
created_date: '2026-04-20 08:49'
updated_date: '2026-04-24 09:49'
created_date: "2026-04-20 08:49"
updated_date: "2026-04-24 09:49"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/173'
- "https://github.com/cloud8421/music_library/issues/173"
priority: low
ordinal: 1000
---
@@ -17,6 +17,7 @@ ordinal: 1000
<!-- SECTION:DESCRIPTION:BEGIN -->
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-16 · updated 2026-04-16_
## Summary
@@ -62,10 +63,13 @@ similar = Artists.get_similar_artists(artist, collected)
```
This removes the `Artists → Collection` edge without moving any behaviour. Within `lib/music_library/`, only `Artists` aliases `Collection` — everything else referencing `Collection` lives in `lib/music_library_web/` (outside the cycle), so this single change is sufficient to break the cycle.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- `mix xref graph --format cycles --label compile` reports no cycle involving Records/Artists/Collection
- `ArtistLive.Show` tests still pass
@@ -76,9 +80,11 @@ This removes the `Artists → Collection` edge without moving any behaviour. Wit
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Removed `alias MusicLibrary.Collection` from `MusicLibrary.Artists` and changed `get_similar_artists/1` to `get_similar_artists/2`, taking the collected artist id set as an argument instead of fetching it inline. The only caller (`ArtistLive.Show`) now computes the set via `Collection.collected_artist_ids()` and passes it in.
`mix xref graph --format cycles --label compile` now reports **no cycles**. The 3-way cycle is gone, and the 15-module compile-connected cycle that previously forced recompilation cascades is broken. A 14-module runtime-only cycle (Records ↔ Artists via Oban-async edges + workers) remains as expected and does not trigger recompilation.
Full test suite (836 tests including 10 in `ArtistLive.ShowTest`) passes. Format and Credo clean.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -3,17 +3,18 @@ id: ML-100
title: Inconsistent timestamp type in ScrobbleRule schema
status: Done
assignee: []
created_date: '2026-04-20 08:58'
created_date: "2026-04-20 08:58"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/74'
- "https://github.com/cloud8421/music_library/issues/74"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-02-17 · updated 2026-03-06 · closed 2026-03-06_
## Priority: Medium
@@ -29,4 +30,5 @@ All schemas should consistently use `timestamps(type: :utc_datetime)`.
## Source
From technical debt audit (2026-02-17), item #1.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-101
title: Artists without Wikipedia page
status: Done
assignee: []
created_date: '2026-04-20 08:58'
created_date: "2026-04-20 08:58"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/73'
- "https://github.com/cloud8421/music_library/issues/73"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-02-11 · updated 2026-03-21 · closed 2026-03-21_
```
@@ -69,4 +70,5 @@ bb00ff20-4f89-407f-8207-28d75145a432|Trojka
e1bfa520-8fad-46de-9678-10912cb26e7f|Overhead
0152651e-5928-47ec-be9c-e4d81c143c67|Retrospective
```
<!-- SECTION:DESCRIPTION:END -->
@@ -3,18 +3,20 @@ id: ML-102
title: Extend Universal Search to search across notes
status: Done
assignee: []
created_date: '2026-04-20 08:58'
created_date: "2026-04-20 08:58"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/72'
- "https://github.com/cloud8421/music_library/issues/72"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-01-08 · updated 2026-02-10 · closed 2026-02-10_
Results should include a small preview of the result.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,18 +3,20 @@ id: ML-103
title: Increase size of latest purchase artwork in stats on mobile
status: Done
assignee: []
created_date: '2026-04-20 08:58'
created_date: "2026-04-20 08:58"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/70'
- "https://github.com/cloud8421/music_library/issues/70"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-12-24 · updated 2025-12-24 · closed 2025-12-24_
Make it slightly bigger, making sure image link uses appropriate dimensions.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,16 +3,18 @@ id: ML-104
title: Setup Litestream to backup database to Hetzner object storage
status: Done
assignee: []
created_date: '2026-04-20 08:58'
created_date: "2026-04-20 08:58"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/69'
- "https://github.com/cloud8421/music_library/issues/69"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-12-12 · updated 2026-03-20 · closed 2026-03-20_
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-105
title: Refactor MusicLibrary.ScrobbleRules
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/65'
- "https://github.com/cloud8421/music_library/issues/65"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-11-16 · updated 2026-02-10 · closed 2026-02-10_
- Extract sql macro for `CASE...WHEN...ELSE` update
@@ -3,18 +3,20 @@ id: ML-106
title: Set up Copilot instructions
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/63'
- "https://github.com/cloud8421/music_library/issues/63"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-11-11 · updated 2025-11-11 · closed 2025-11-11_
Configure instructions for this repository as documented in Best practices for Copilot coding agent in your repository.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-107
title: Regular job to cleanup asset cache
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/59'
- "https://github.com/cloud8421/music_library/issues/59"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-11-07 · updated 2025-11-07 · closed 2025-11-07_
This query returns the hash of all orphan assets
@@ -25,4 +26,5 @@ LEFT JOIN records on records.cover_hash == assets.hash
LEFT JOIN artist_infos on artist_infos.image_data_hash == assets.hash
WHERE records.id IS NULL AND artist_infos.id IS NULL;
```
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-108
title: Support universal search navigation via keyboard
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/58'
- "https://github.com/cloud8421/music_library/issues/58"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-11-01 · updated 2025-11-09 · closed 2025-11-09_
The universal search modal cannot be navigated by keyboard only. It should support:
@@ -24,4 +25,5 @@ The universal search modal cannot be navigated by keyboard only. It should suppo
- When pressing down on the last result, go back to the search input
Should be implemented as a hook for the Universal Search component.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,36 +3,41 @@ id: ML-109
title: Improve test suite
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/57'
- "https://github.com/cloud8421/music_library/issues/57"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-10-25 · updated 2026-02-07 · closed 2026-02-07_
Comprehensive test coverage analysis. Key gaps identified:
**Critical (High Priority):**
- 9 of 10 background workers untested (FetchArtistImage, FetchArtistInfo, RecordRefreshMusicBrainzData, RefreshCover, PopulateGenres, GenerateRecordEmbedding, PruneArtistInfo, PruneAssetCache, ExtractColors)
- Scrobble LiveView — user-facing feature with Last.fm integration
- LastFmController — OAuth authentication flow
**Moderate Priority:**
- Universal Search LiveView
- ArchiveController (backup download)
- HealthController
**Low Priority:**
- Online Store Templates LiveView
- Notes management
- Stats detail LiveViews (top albums/artists)
- Color extraction feature
**Well-tested areas:** Collection/Wishlist/Artist/Stats LiveViews, core context modules (Records, Collection, Wishlist, Artists, ScrobbleActivity, ScrobbleRules), external API clients (MusicBrainz, LastFm, Discogs), AssetController, CollectionController, SessionController, Auth plug.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,18 +3,19 @@ id: ML-11
title: Telemetry.Storage GenServer funnels synchronous SQLite writes
status: Done
assignee: []
created_date: '2026-04-20 08:49'
updated_date: '2026-04-24 09:29'
created_date: "2026-04-20 08:49"
updated_date: "2026-04-24 09:29"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/172'
- "https://github.com/cloud8421/music_library/issues/172"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-16 · updated 2026-04-16_
## Summary
@@ -42,7 +43,9 @@ Issue #105 was closed as NOT_PLANNED with the rationale that bare catches are "r
Whichever option is chosen, replace the bare `catch _, _` with either a targeted `rescue` or `Logger.debug` so failures become observable.
## Acceptance Criteria
<!-- AC:BEGIN -->
- Telemetry write path does not block on the GenServer mailbox
- Error path surfaces at least once per distinct failure (not silently swallowed)
- Existing dashboard queries continue to work
+4 -2
View File
@@ -3,18 +3,20 @@ id: ML-110
title: Radios?
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/56'
- "https://github.com/cloud8421/music_library/issues/56"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-10-17 · updated 2026-02-10 · closed 2026-02-10_
For ease of use, one could play radios directly from the library, with bonus points for scrobbling tracks.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,18 +3,20 @@ id: ML-111
title: Review design of ad-hoc scrobble show view
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/54'
- "https://github.com/cloud8421/music_library/issues/54"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-09-28 · updated 2026-02-10 · closed 2026-02-10_
Metadata part is AI-generated and not-consistent with rest of the application.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,20 +3,22 @@ id: ML-112
title: Optimise scrobble rule application
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/53'
- "https://github.com/cloud8421/music_library/issues/53"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-09-28 · updated 2025-11-10 · closed 2025-11-10_
When applying all rules, we currently apply each rule independently on the entire database. This means that it takes a significant amount of time to apply all rules.
Instead we should be able to compose a single query to apply all album rules, and a single query to apply all artist rules.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-113
title: Scrobble groups of tracks
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/52'
- "https://github.com/cloud8421/music_library/issues/52"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-09-28 · updated 2025-11-11 · closed 2025-11-11_
When scrobbling a collected release from the release sheet, it's only possible to scrobble either the entire release or an individual medium.
@@ -3,18 +3,20 @@ id: ML-114
title: Display progarchives links for records
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/49'
- "https://github.com/cloud8421/music_library/issues/49"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-25 · updated 2026-02-10 · closed 2026-02-10_
Where applicable.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,18 +3,20 @@ id: ML-115
title: Import Last.fm history
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/48'
- "https://github.com/cloud8421/music_library/issues/48"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-22 · updated 2025-05-31 · closed 2025-05-31_
When polling Last.fm, store scrobbles locally in a dedicated table. Add functionality to scrape past data as well.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,16 +3,18 @@ id: ML-116
title: Display similar artists with an image grid
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/47'
- "https://github.com/cloud8421/music_library/issues/47"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-21 · updated 2025-05-21 · closed 2025-05-21_
<!-- SECTION:DESCRIPTION:END -->
@@ -3,18 +3,20 @@ id: ML-117
title: Move artist bio to a left side sheet
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/46'
- "https://github.com/cloud8421/music_library/issues/46"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-21 · updated 2025-05-21 · closed 2025-05-21_
Display summary on left-hand side, with a read more button that opens the sheet.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,16 +3,18 @@ id: ML-118
title: Add actions to refresh artist info and artwork from artist page
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/45'
- "https://github.com/cloud8421/music_library/issues/45"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-20 · updated 2025-05-20 · closed 2025-05-20_
<!-- SECTION:DESCRIPTION:END -->
@@ -3,18 +3,20 @@ id: ML-119
title: Scrobble individual media
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/44'
- "https://github.com/cloud8421/music_library/issues/44"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-09 · updated 2025-05-10 · closed 2025-05-10_
So that it's possible to scrobble individual discs.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-12
title: Use Notes.change_note/2 instead of Note.changeset/2 in notes component
status: Done
assignee: []
created_date: '2026-04-20 08:49'
created_date: "2026-04-20 08:49"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/171'
- "https://github.com/cloud8421/music_library/issues/171"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-16 · updated 2026-04-17 · closed 2026-04-17_
## Summary
@@ -37,7 +38,9 @@ The "LiveViews / LiveComponents call the context, not schema modules" boundary i
Mechanical find-and-replace. Three one-line edits.
## Acceptance Criteria
<!-- AC:BEGIN -->
- Tests still pass
- No direct `Note.changeset` references in `lib/music_library_web/components/notes.ex`
<!-- SECTION:DESCRIPTION:END -->
@@ -1,18 +1,20 @@
---
id: ML-120
title: 'When scanning a wish listed record, set its selected release'
title: "When scanning a wish listed record, set its selected release"
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/43'
- "https://github.com/cloud8421/music_library/issues/43"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-09 · updated 2025-05-09 · closed 2025-05-09_
<!-- SECTION:DESCRIPTION:END -->
@@ -3,18 +3,20 @@ id: ML-121
title: Disable Last.fm http pool
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/42'
- "https://github.com/cloud8421/music_library/issues/42"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-09 · updated 2025-05-14 · closed 2025-05-14_
Doesn't recover well after the vm resumes from suspension, and causes the first http request to Last.fm to always fail.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,16 +3,18 @@ id: ML-122
title: Increase size of record form save button on mobile
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/41'
- "https://github.com/cloud8421/music_library/issues/41"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-09 · updated 2025-05-09 · closed 2025-05-09_
<!-- SECTION:DESCRIPTION:END -->
@@ -3,18 +3,20 @@ id: ML-124
title: Improve display of releases in the edit record dropdown
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/39'
- "https://github.com/cloud8421/music_library/issues/39"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-07 · updated 2025-05-07 · closed 2025-05-07_
Sort by date descending, country
<!-- SECTION:DESCRIPTION:END -->
@@ -3,16 +3,18 @@ id: ML-125
title: Display total runtime for the entire release
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/38'
- "https://github.com/cloud8421/music_library/issues/38"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-04 · updated 2025-05-05 · closed 2025-05-05_
<!-- SECTION:DESCRIPTION:END -->
@@ -1,18 +1,20 @@
---
id: ML-126
title: 'For compilations, each track should also display artist(s)'
title: "For compilations, each track should also display artist(s)"
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/37'
- "https://github.com/cloud8421/music_library/issues/37"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-04 · updated 2025-05-04 · closed 2025-05-04_
<!-- SECTION:DESCRIPTION:END -->
@@ -1,18 +1,20 @@
---
id: ML-127
title: 'Display tracks, each one with title and duration (in mm:ss)'
title: "Display tracks, each one with title and duration (in mm:ss)"
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/36'
- "https://github.com/cloud8421/music_library/issues/36"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-04 · updated 2025-05-05 · closed 2025-05-05_
<!-- SECTION:DESCRIPTION:END -->
@@ -1,20 +1,22 @@
---
id: ML-128
title: 'Display media (discs), ordered by position with total runtime'
title: "Display media (discs), ordered by position with total runtime"
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/35'
- "https://github.com/cloud8421/music_library/issues/35"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-04 · updated 2025-05-05 · closed 2025-05-05_
Display media (A.K.A. discs), ordered by their `position` but identified by their label, with the total runtime
<!-- SECTION:DESCRIPTION:END -->
@@ -3,18 +3,20 @@ id: ML-129
title: Display tracklist sheet from record detail page
status: Done
assignee: []
created_date: '2026-04-20 08:59'
created_date: "2026-04-20 08:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/34'
- "https://github.com/cloud8421/music_library/issues/34"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-04 · updated 2025-05-04 · closed 2025-05-04_
In the records detail page, display a sheet on the right hand side, activated by a track list icon in the same row as the collected release information.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,18 +3,19 @@ id: ML-13
title: Tighten ecto_sqlite3 version constraint in mix.exs
status: Done
assignee: []
created_date: '2026-04-20 08:50'
updated_date: '2026-04-24 08:53'
created_date: "2026-04-20 08:50"
updated_date: "2026-04-24 08:53"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/170'
- "https://github.com/cloud8421/music_library/issues/170"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-16 · updated 2026-04-16_
## Summary
@@ -34,7 +35,9 @@ All other production deps use `~>` with at least a major pin (e.g. `ecto_sql, "~
Change to `{:ecto_sqlite3, "~> 0.22"}`. Matches the installed major; prevents accidental upgrade on a fresh `mix deps.get`.
## Acceptance Criteria
<!-- AC:BEGIN -->
- `mix deps.get` still resolves cleanly
- `mix test` passes
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-130
title: Enrich selected release input and display
status: Done
assignee: []
created_date: '2026-04-20 09:00'
created_date: "2026-04-20 09:00"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/33'
- "https://github.com/cloud8421/music_library/issues/33"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-05-01 · updated 2025-05-25 · closed 2025-05-25_
- Needs to be easy to visually scan by format and date.
@@ -21,4 +22,5 @@ _GitHub: created 2025-05-01 · updated 2025-05-25 · closed 2025-05-25_
- Input needs to be searchable.
Optionally (if possible) group options in input so that releases of the same format are presented first.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,20 +3,22 @@ id: ML-131
title: Connect a Last.fm account
status: Done
assignee: []
created_date: '2026-04-20 09:00'
created_date: "2026-04-20 09:00"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/32'
- "https://github.com/cloud8421/music_library/issues/32"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-04-30 · updated 2025-05-07 · closed 2025-05-07_
Currently communication with Last.fm is done via an API token, and the user is configured with a static username.
Supporting the association with a Last.fm account would support existing functionality, but also open the door for future larger integration with Last.fm.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,16 +3,18 @@ id: ML-132
title: Scrobble a record to Last.fm
status: Done
assignee: []
created_date: '2026-04-20 09:00'
created_date: "2026-04-20 09:00"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/31'
- "https://github.com/cloud8421/music_library/issues/31"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-04-30 · updated 2025-05-07 · closed 2025-05-07_
<!-- SECTION:DESCRIPTION:END -->
@@ -3,18 +3,20 @@ id: ML-133
title: Show a record track list with durations
status: Done
assignee: []
created_date: '2026-04-20 09:00'
created_date: "2026-04-20 09:00"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/30'
- "https://github.com/cloud8421/music_library/issues/30"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-04-30 · updated 2025-05-05 · closed 2025-05-05_
Only for collected records with a selected release.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,18 +3,20 @@ id: ML-134
title: Support selecting a release per record
status: Done
assignee: []
created_date: '2026-04-20 09:00'
created_date: "2026-04-20 09:00"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/29'
- "https://github.com/cloud8421/music_library/issues/29"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-04-30 · updated 2025-05-01 · closed 2025-05-01_
While records themselves map to MusicBrainz release groups, associating a record with a release opens the door to two major future improvements.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,18 +3,20 @@ id: ML-135
title: Add country to artist page
status: Done
assignee: []
created_date: '2026-04-20 09:00'
created_date: "2026-04-20 09:00"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/28'
- "https://github.com/cloud8421/music_library/issues/28"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-04-28 · updated 2025-04-29 · closed 2025-04-29_
Provided by Musicbrainz data. Can be rendered via an emoji flag via flagmojis.
<!-- SECTION:DESCRIPTION:END -->
+4 -2
View File
@@ -3,18 +3,20 @@ id: ML-136
title: Unicode search
status: Done
assignee: []
created_date: '2026-04-20 09:00'
created_date: "2026-04-20 09:00"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/27'
- "https://github.com/cloud8421/music_library/issues/27"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-04-26 · updated 2025-05-25 · closed 2025-05-25_
Searching for "bjorn" should return records, but it doesn't.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-137
title: Polyfill missing data in scrobble activity
status: Done
assignee: []
created_date: '2026-04-20 09:00'
created_date: "2026-04-20 09:00"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/26'
- "https://github.com/cloud8421/music_library/issues/26"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-04-26 · updated 2025-09-26 · closed 2025-09-26_
- For tracked records, display the record artwork rather than the one provided by Last.fm.
@@ -3,18 +3,20 @@ id: ML-138
title: Allow multiple plays of the same album in scrobble activity
status: Done
assignee: []
created_date: '2026-04-20 09:00'
created_date: "2026-04-20 09:00"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/25'
- "https://github.com/cloud8421/music_library/issues/25"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-04-24 · updated 2025-04-25 · closed 2025-04-25_
The logic that packs scrobbled tracks into albums currently keeps only one occurrence per album (the most recent), which ends up showing an incorrect history. An album can be played multiple non-contiguous times in the same window.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,18 +3,20 @@ id: ML-139
title: Polyfill missing artist IDs in scrobble activity
status: Done
assignee: []
created_date: '2026-04-20 09:00'
created_date: "2026-04-20 09:00"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/24'
- "https://github.com/cloud8421/music_library/issues/24"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-04-23 · updated 2025-04-25 · closed 2025-04-25_
Given that each scrobbled track can be matched to a collected or wishlisted release, it's possible to polyfill missing artist information by copying over the data from the album artist(s).
<!-- SECTION:DESCRIPTION:END -->
@@ -1,19 +1,20 @@
---
id: ML-14
title: 'Replace Process.sleep with :sys.get_state in error_notifier_test.exs'
title: "Replace Process.sleep with :sys.get_state in error_notifier_test.exs"
status: Done
assignee: []
created_date: '2026-04-20 08:50'
created_date: "2026-04-20 08:50"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/169'
- "https://github.com/cloud8421/music_library/issues/169"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-16 · updated 2026-04-17 · closed 2026-04-17_
## Summary
@@ -31,7 +32,9 @@ Each call is after `:telemetry.execute/3`, waiting for the `ErrorTracker.ErrorNo
Replace each `Process.sleep(50)` with `:sys.get_state(ErrorTracker.ErrorNotifier)` — a synchronous probe that blocks until the GenServer has drained its mailbox up to the current message. Deterministic and faster.
## Acceptance Criteria
<!-- AC:BEGIN -->
- Zero `Process.sleep` occurrences in `test/`
- Tests remain deterministic and pass under repeated runs
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-140
title: Get artist images
status: Done
assignee: []
created_date: '2026-04-20 09:00'
created_date: "2026-04-20 09:00"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/23'
- "https://github.com/cloud8421/music_library/issues/23"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2025-04-22 · updated 2025-04-29 · closed 2025-04-29_
Fetch artist images via MusicBrainz url-rels → Discogs artist endpoint. Implementation checklist (all completed):
@@ -3,17 +3,18 @@ id: ML-141
title: Support records with accents
status: Done
assignee: []
created_date: '2026-04-20 09:00'
created_date: "2026-04-20 09:00"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/7'
- "https://github.com/cloud8421/music_library/issues/7"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2024-12-07 · updated 2024-12-07 · closed 2024-12-07_
SQLite's collation functions don't by default collapse accented characters into their non-accented variants. For example, an artist with sort name `Åkerfeldt, Mikael` is appended at the bottom instead of being grouped with `A`.
@@ -21,4 +22,5 @@ SQLite's collation functions don't by default collapse accented characters into
The `records_search_index` table behaves correctly (searching `Aker` matches the artist), but alphabetical sorting/grouping is broken for non-ASCII sort names.
Fix: ensure non-ASCII artist records are slotted at the correct alphabetical position.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,8 +3,8 @@ id: ML-142
title: Improve scrobble UI in the Release component
status: Done
assignee: []
created_date: '2026-04-20 09:32'
updated_date: '2026-04-22 13:27'
created_date: "2026-04-20 09:32"
updated_date: "2026-04-22 13:27"
labels:
- ui
- scrobble
@@ -24,11 +24,15 @@ ordinal: 1000
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
The Release component's scrobble interface has several usability gaps that make scrobbling cumbersome, especially for multi-medium releases. This task improves the experience across three areas: custom scrobble time, button visual clarity, and per-medium scrobble access.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 The release-sheet header renders a `Finished at` date/time picker that displays 'Now' when unset and an explicit time when set
- [x] #2 The release-sheet header renders a solid-primary `Scrobble release` button that scrobbles the whole release using the picker value or `DateTime.utc_now()` when unset
- [x] #3 The release-sheet header renders a `⋯` overflow menu containing `Print tracklist`, plus `Connect Last.fm` when the session key is missing
@@ -50,9 +54,11 @@ The Release component's scrobble interface has several usability gaps that make
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implementation complete 2026-04-22.
Changes:
- `lib/music_library_web/components/release.ex` — header restructure (title + subtitle + picker + solid `Scrobble release` + ⋯ dropdown), `.medium/1` updated (removed selection-blocks-medium disable, label visible, print moved to ⋯ dropdown), new `.selection_bar/1` function component + private `selected_tracks_summary/2` helper, `finished_at` wired into the form via `parse_finished_at/1`, new `clear_finished_at` event handler, and all three scrobble handlers now resolve `socket.assigns.finished_at || DateTime.utc_now()` at call time.
- `test/music_library_web/live/collection_live/show_test.exs` — updated assertion from "Connect your Last.fm account" to new "Connect Last.fm" link label.
- `test/music_library_web/live/scrobble_live/show_test.exs` — regression test added: medium scrobble works with a cross-medium track selected.
@@ -60,6 +66,7 @@ Changes:
- `priv/gettext/default.pot` + `priv/gettext/en/LC_MESSAGES/default.po` — regenerated via `mix gettext.extract --merge`.
Verification:
- `mise run dev:precommit` — all green (credo, sobelow, formatting, translations, 823 tests passing).
- Browser-verified at :4003: desktop 1440px (4-disc release showed new header, per-medium buttons, sticky bar with cross-medium count), mobile 360px (header stacks to title + picker + Release button, medium scrobble collapses to icon-only), picker open/select/reset cycle worked, overflow menus rendered Print tracklist.
- `grep -n "MapSet.size(@selected_tracks) > 0" lib/music_library_web/components/release.ex` returns only the sticky-bar visibility guard, as planned.
@@ -3,8 +3,8 @@ id: ML-143
title: Cart-style multi-record import in Add Record modal
status: Done
assignee: []
created_date: '2026-04-20 10:00'
updated_date: '2026-04-20 12:34'
created_date: "2026-04-20 10:00"
updated_date: "2026-04-20 12:34"
labels:
- ui
- liveview
@@ -23,9 +23,11 @@ ordinal: 1000
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Replace the one-record-at-a-time import in the Add Record modal with a shopping-cart staging flow. Users build an ephemeral cart of `{release_group, format}` items from MusicBrainz search results, then import them all at once.
Batch-size behaviour mirrors the existing `BarcodeScan` split:
- 1 item → sync `start_async` with a spinner in the "Import 1 record" button; on success the modal closes and navigates to the new record.
- 2+ items → one Oban job per item via a new `ImportFromMusicbrainzReleaseGroup` worker; modal closes immediately and toasts "Importing N records in the background..."
@@ -34,10 +36,13 @@ Layout: mockup B (bottom tray) as baseline, mockup A (search left, cart right) o
Full technical plan and visual mockups are in the task folder — see References.
Affects both `CollectionLive.Index` and `WishlistLive.Index`. Barcode scanner flow and `StatsLive` single-record import are untouched.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 From the Collection or Wishlist index, clicking a format in the result dropdown adds a `{release_group, format}` item to the cart instead of importing immediately
- [ ] #2 The cart renders as a collapsible bottom tray on small viewports and as a right-hand panel on `md:` and up
- [ ] #3 The import modal widens on `md:` and up to fit the side-by-side layout
@@ -3,8 +3,8 @@ id: ML-145
title: Restructure /scrobble routes; reuse Release component on scrobble page
status: Done
assignee: []
created_date: '2026-04-23 06:11'
updated_date: '2026-04-23 14:15'
created_date: "2026-04-23 06:11"
updated_date: "2026-04-23 14:15"
labels:
- ui
- scrobble
@@ -28,6 +28,7 @@ priority: medium
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
## Why
`/scrobble/:release_id` (`MusicLibraryWeb.ScrobbleLive.Show`) re-implements scrobbling UI that already exists as a LiveComponent used by collection/wishlist show pages (`MusicLibraryWeb.Components.Release`). Today the scrobble page imports `medium/1` and `scrobble_button_label/1` from that component but reimplements `scrobble_release` / `scrobble_medium` / `scrobble_selected_tracks` with hard-coded `DateTime.utc_now/0`, no `Finished at` picker, and no selection bar. ML-142.1 proposed porting those affordances into `ScrobbleLive.Show` in parallel — which would leave two drifting implementations.
@@ -67,7 +68,9 @@ Old `/scrobble/:release_id` → 404 (personal app; bookmark churn is acceptable,
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 /scrobble/:rg_id renders a release-group header (cover, release-group title, primary artist, type badge, first-release date, release count) and the full list of releases returned by MusicBrainz for that group.
- [x] #2 Each release in the /scrobble/:rg_id list is a navigate link to /scrobble/:rg_id/releases/:release_id.
- [x] #3 /scrobble/:rg_id shows an error toast and redirects to /scrobble if fetching the release group or its releases fails.
@@ -90,6 +93,7 @@ Old `/scrobble/:release_id` → 404 (personal app; bookmark churn is acceptable,
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
## Summary
Restructured `/scrobble` into a three-level URL hierarchy (`/scrobble``/scrobble/:rg_id``/scrobble/:rg_id/releases/:release_id`) and eliminated the duplicated scrobble UI by making the Release LiveComponent reusable on a standalone page.
@@ -136,6 +140,7 @@ Two bugs surfaced during Phase 8 verification:
- `mise run dev:precommit`: clean (shellcheck, credo, sobelow, translations, formatting, unused deps, full test run).
New coverage:
- `test/music_library_web/live/scrobble_live/release_group_show_test.exs` — happy path (header + release links) and fetch failure (toast + redirect to `/scrobble`).
- `test/music_library_web/live/scrobble_live/release_show_test.exs` — smoke (mount + component render + back link target).
- `show_print?={true|false}` verified via `Phoenix.LiveViewTest.live_isolated/3` in `release_test.exs`.
@@ -145,6 +150,7 @@ Deleted: `test/music_library_web/live/scrobble_live/show_test.exs` (replaced by
## Manual UI verification (Phase 8 AC#17)
Confirmed by user in dev server:
- Navigation loop `/scrobble``/scrobble/:rg_id``/scrobble/:rg_id/releases/:release_id` works; `?query=X` survives browser back.
- Scrobble release page: picker, per-release/medium/track scrobble, and selection bar sticky at viewport bottom.
- Collection show sheet: selection bar pins to sheet bottom while tracks scroll.
@@ -4,8 +4,8 @@ title: Honour Retry-After and rate-limit reset headers for precise snooze
status: Done
assignee:
- Codex
created_date: '2026-04-24 11:12'
updated_date: '2026-04-27 21:02'
created_date: "2026-04-24 11:12"
updated_date: "2026-04-27 21:02"
labels: []
dependencies:
- ML-21
@@ -16,20 +16,21 @@ ordinal: 1000
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Follow-up to ML-21.
Once workers classify API errors as transient vs permanent, the next refinement is using per-response retry hints instead of fixed snooze durations.
## What each API provides
| API | Header | Format |
|-----|--------|--------|
| MusicBrainz | `Retry-After` | seconds (on 503) |
| Wikipedia REST v1 / Action API | `Retry-After` | seconds (on 429 / 503 from `maxlag`) |
| Discogs | `X-Discogs-Ratelimit-Remaining` + rolling 60s window | no `Retry-After`; fallback to a fixed backoff |
| Brave Search | `X-RateLimit-Reset` | seconds until reset (comma-separated for multi-window policies) |
| OpenAI | `x-ratelimit-reset-requests`, `x-ratelimit-reset-tokens` | duration string (e.g. `120ms`, `2s`) |
| Last.fm | none | keep existing `ErrorResponse.retry_delay/1` fallback |
| API | Header | Format |
| ------------------------------ | -------------------------------------------------------- | --------------------------------------------------------------- |
| MusicBrainz | `Retry-After` | seconds (on 503) |
| Wikipedia REST v1 / Action API | `Retry-After` | seconds (on 429 / 503 from `maxlag`) |
| Discogs | `X-Discogs-Ratelimit-Remaining` + rolling 60s window | no `Retry-After`; fallback to a fixed backoff |
| Brave Search | `X-RateLimit-Reset` | seconds until reset (comma-separated for multi-window policies) |
| OpenAI | `x-ratelimit-reset-requests`, `x-ratelimit-reset-tokens` | duration string (e.g. `120ms`, `2s`) |
| Last.fm | none | keep existing `ErrorResponse.retry_delay/1` fallback |
## Why this is a follow-up and not part of ML-21
@@ -45,11 +46,14 @@ ML-21 gets us from "no classification" to `:retry | :cancel`. That alone lets wo
## Dependencies
Blocked by ML-21 — requires the classification plumbing to exist first.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Each API module extracts a retry-delay value from its response (Retry-After, X-RateLimit-Reset, x-ratelimit-reset-*) when present
- [x] #1 Each API module extracts a retry-delay value from its response (Retry-After, X-RateLimit-Reset, x-ratelimit-reset-\*) when present
- [x] #2 Workers emit {:snooze, seconds} with the parsed value for transient errors, falling back to a fixed default when the header is absent or malformed
- [x] #3 Parsed durations are clamped to a safe range to prevent pathological values
- [x] #4 Per-API header parsing is covered by unit tests using representative fixture responses
@@ -58,6 +62,7 @@ Blocked by ML-21 — requires the classification plumbing to exist first.
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
# Implementation Plan
- Add a shared `MusicLibrary.RetryDelay` helper that parses retry/reset headers from `Req.Response` values, clamps parsed provider hints to 5..300 seconds, uses the maximum valid value for multi-window headers, and returns nil for absent or malformed hints.
@@ -70,14 +75,17 @@ Blocked by ML-21 — requires the classification plumbing to exist first.
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented precise retry-delay parsing through the existing ErrorResponse callback flow. Added MusicLibrary.RetryDelay with 5..300s clamping and max-window selection for multi-window headers. MusicBrainz/Wikipedia parse Retry-After, Brave parses X-RateLimit-Reset, OpenAI parses request/token reset durations. Discogs and Last.fm remain on fixed fallbacks because they do not expose a reliable retry-delay header in scope. Verification passed: focused API/error-handler tests, full mix test suite, mix format --check-formatted, and git diff --check.
Addressed review gaps after implementation: Wikipedia Action API error promotion now passes the full Req response to `from_action_api_body/2` so `Retry-After` is preserved; OpenAI retry parsing now includes `Retry-After` and compound reset durations such as `1m30s`; `Retry-After: 0` now clamps to the 5s minimum instead of falling back. Verification passed: focused retry/API tests, full `mix test`, and `mix format --check-formatted`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
## Summary
Added shared retry-delay parsing for provider retry/reset headers and wired it into the existing structured API error flow. HTTP-based ErrorResponse structs for MusicBrainz, Wikipedia, Brave Search, and OpenAI now capture an optional parsed retry delay and prefer it from `retry_delay_seconds/1`; workers automatically emit `{:snooze, parsed_seconds}` through the existing `MusicLibrary.Worker.ErrorHandler` path.
@@ -3,8 +3,8 @@ id: ML-147
title: Upgrade to ServerSentEvents 1.0
status: Done
assignee: []
created_date: '2026-04-25 21:26'
updated_date: '2026-04-27 21:02'
created_date: "2026-04-25 21:26"
updated_date: "2026-04-27 21:02"
labels: []
dependencies: []
priority: medium
@@ -14,10 +14,14 @@ ordinal: 2000
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Allows potential drastic simplification of `OpenAI.API.chat_stream/6` following [this example](https://github.com/benjreinhart/server_sent_events#real-world-example-using-req).
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 Plain refactor - dependency is updated and functionality keeps working as expected
<!-- AC:END -->
@@ -4,8 +4,8 @@ title: Fix release selection bar text visibility in Safari
status: Done
assignee:
- Codex
created_date: '2026-04-30 06:11'
updated_date: '2026-04-30 06:20'
created_date: "2026-04-30 06:11"
updated_date: "2026-04-30 06:20"
labels:
- bug
- ui
@@ -18,11 +18,15 @@ priority: medium
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
When multiple tracks are selected in the release UI, the `selection_bar` component should show its left-hand selection text consistently across supported browsers. The current behavior shows the text in Chrome but leaves it invisible in Safari.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 The left-hand selection text is visible in Safari when multiple tracks are selected.
- [x] #2 The existing Chrome rendering remains unchanged or visually equivalent.
- [x] #3 The fix is scoped to the release selection bar and does not alter unrelated release controls.
@@ -32,6 +36,7 @@ When multiple tracks are selected in the release UI, the `selection_bar` compone
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Read the project conventions and inspect `lib/music_library_web/components/release.ex` plus nearby CSS/assets that affect `selection_bar`.
2. Identify the Safari-specific style interaction causing the left selection text to be invisible when multiple tracks are selected.
3. Apply the smallest scoped change to the release selection bar so the text remains visible in Safari while preserving Chrome rendering.
@@ -41,6 +46,7 @@ When multiple tracks are selected in the release UI, the `selection_bar` compone
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Changed the selection bar wrapper from the non-existent `isolated` class to Tailwind's `isolate` utility, giving the sticky bar an explicit `isolation: isolate` stacking context. This is scoped to `lib/music_library_web/components/release.ex` and targets Safari's sticky/composited paint issue where the left summary text was not visible. Ran `mix format lib/music_library_web/components/release.ex` and `mix test test/music_library_web/components/release_test.exs` successfully. Manual Safari browser verification was not available in this environment.
User reported that changing `isolated` to `isolate` did not fix the Safari visibility issue. Reopened the task for further investigation.
@@ -48,14 +54,17 @@ User reported that changing `isolated` to `isolate` did not fix the Safari visib
Applied a second scoped patch after the first attempt failed: the sticky selection bar now uses `transform-gpu` to force a composited layer in WebKit, and the left text block uses `relative z-10 flex-auto` instead of `flex-1` so Safari has an explicit paint/layer order and content-based flex basis. `mix format lib/music_library_web/components/release.ex` and `mix test test/music_library_web/components/release_test.exs` both pass. Waiting for user confirmation before checking the Safari acceptance criteria or closing the task.
User isolated the fix in Safari: `relative` on the selection bar's left text container fixes the invisible text. The earlier `isolate`, `transform-gpu`, `z-10`, and `flex-auto` changes were unnecessary and have been removed. The final code change is only `relative` on the existing `min-w-0 flex-1 leading-tight` container. Ran `mix format lib/music_library_web/components/release.ex` and `mix test test/music_library_web/components/release_test.exs` successfully.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Added `relative` to the left text container inside the release `selection_bar`. Safari was failing to visibly paint that text when tracks were selected; the positioned container fixes the rendering issue without changing the selection bar layout, colors, or actions.
Verification:
- User confirmed in Safari that `relative` is the change that fixes the issue.
- `mix format lib/music_library_web/components/release.ex`
- `mix test test/music_library_web/components/release_test.exs`
@@ -3,17 +3,18 @@ id: ML-15
title: Sanitize Wikipedia bio_html in ArtistLive.Show
status: Done
assignee: []
created_date: '2026-04-20 08:50'
created_date: "2026-04-20 08:50"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/168'
- "https://github.com/cloud8421/music_library/issues/168"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-16 · updated 2026-04-16 · closed 2026-04-16_
## Summary
@@ -34,11 +35,14 @@ Low under the single-user threat model (Wikipedia's REST API returns sanitized H
## Fix
Either:
1. Pipe `bio_html` through `MDEx.safe_html/2` with `MDEx.Document.default_sanitize_options()` (strongest), or
2. Add `# sobelow_skip ["XSS.Raw"]` at `show.ex:298` with a comment explaining Wikipedia is a trusted third-party HTML source (consistent with the other 7 annotation sites).
## Acceptance Criteria
<!-- AC:BEGIN -->
- Either HTML sanitization applied, or annotation + justification added
- Sobelow scan stays clean at `--exit high`
<!-- SECTION:DESCRIPTION:END -->
@@ -3,8 +3,8 @@ id: ML-150
title: Extract Records sub-contexts to reduce module size
status: Done
assignee: []
created_date: '2026-04-30 10:47'
updated_date: '2026-04-30 16:08'
created_date: "2026-04-30 10:47"
updated_date: "2026-04-30 16:08"
labels:
- refactor
- records
@@ -17,9 +17,11 @@ priority: medium
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
The `Records` context module (450+ lines in `lib/music_library/records.ex`) handles CRUD, FTS5 search, MusicBrainz import, cover management, genre population, color extraction, PubSub notifications, and similarity embedding dispatch — too many responsibilities for a single module.
Extract focused sub-contexts:
- `Records.Search` — FTS5 search, `SearchParser` integration, search result formatting
- `Records.Import` — MusicBrainz release/group import, barcode scan integration
- `Records.Enrichment` — genre population, color extraction, cover management, embedding dispatch
@@ -27,10 +29,13 @@ Extract focused sub-contexts:
Keep the public `Records` module as a facade that re-exports key functions for backward compatibility with all existing callers (LiveViews, workers, controllers).
The `SearchIndex` schema, `Record` schema, `Similarity` module, `TracklistPdf`, and `Batch` sub-modules stay as-is. Only `records.ex` itself is being split.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 `Records.Search`, `Records.Import`, and `Records.Enrichment` modules exist with focused responsibilities
- [x] #2 Public `Records` module re-exports all previously-public functions through delegation
- [x] #3 All callers (LiveViews, workers, controllers, tests) continue to work without changes to their import/alias lines
@@ -41,22 +46,26 @@ The `SearchIndex` schema, `Record` schema, `Similarity` module, `TracklistPdf`,
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
## Implementation Summary
### New modules created
**`Records.Search`** (`lib/music_library/records/search.ex`):
- `essential_fields/0`, `search_records/3`, `search_records_count/2`, `list_genres/0`
- Private: `build_search/3`, `fts_escape/1`, `fts_query_escape/1`
- Imports `order_alphabetically` macro from `Records` (one-direction dependency, no cycle)
**`Records.Import`** (`lib/music_library/records/import.ex`):
- `get_release_status/2`, `get_artist_records/1`
- `import_from_musicbrainz_release/2`, `import_from_musicbrainz_release_group/2`
- Private: `get_cover_art_or_default/1`, `build_record_attrs/2`
- Calls `Records.create_record/1` and `Records.Search.essential_fields/0`
**`Records.Enrichment`** (`lib/music_library/records/enrichment.ex`):
- `populate_genres/1`, `populate_genres_async/1`
- `refresh_cover/1`, `refresh_cover_async/1`
- `extract_colors/1`, `resize_cover/1`
@@ -66,11 +75,13 @@ The `SearchIndex` schema, `Record` schema, `Similarity` module, `TracklistPdf`,
- Calls `Records.update_record/2`
### Facade (`lib/music_library/records.ex`)
- 15 `defdelegate` calls covering all moved functions (compile-time safety)
- `order_alphabetically` macro stays in `Records` for backward compatibility
- CRUD (`get_record`, `create_record`, `update_record`, `delete_record`, `change_record`) and PubSub (`subscribe`, `notify_update`) stay directly in `Records`
### Dependency graph (one direction only):
```
Records.Search → imports macro from Records
Records.Import → calls Records, Records.Search
@@ -79,6 +90,7 @@ Records → delegates to Search, Import, Enrichment (compile-time: defdelegate o
```
### Tests
- `test/music_library/records_test.exs` — CRUD tests (6 tests)
- `test/music_library/records/search_test.exs` — search tests (13 tests)
- `test/music_library/records/import_test.exs` — import tests (3 tests)
@@ -3,8 +3,8 @@ id: ML-151
title: Document Assets.Cache TTL and cache invalidation strategy
status: Done
assignee: []
created_date: '2026-04-30 10:48'
updated_date: '2026-04-30 12:04'
created_date: "2026-04-30 10:48"
updated_date: "2026-04-30 12:04"
labels:
- documentation
- assets
@@ -17,18 +17,22 @@ priority: low
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
`Assets.Cache` (`lib/music_library/assets/cache.ex`) uses an ETS table with TTL for caching binary asset data. When assets are updated (e.g., a new cover is uploaded), the cache relies on TTL-based expiry — there is no explicit invalidation.
The TTL duration and the rationale for TTL-only invalidation (vs. explicit purge) are not documented in the module or in the architecture docs.
Add a `@moduledoc` to `Assets.Cache` that explains:
- The TTL value and where it's configured
- The invalidation strategy (TTL-based expiry) and why explicit invalidation is/isn't needed
- Update `docs/architecture.md` if the design decision has architectural relevance
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 `Assets.Cache` `@moduledoc` documents the TTL value and where it is configured
- [x] #2 The invalidation strategy (TTL-based expiry) is explained with rationale
- [x] #3 Any architectural implications are captured in `docs/architecture.md`
@@ -37,6 +41,7 @@ Add a `@moduledoc` to `Assets.Cache` that explains:
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
## Plan
### What we're documenting
@@ -52,6 +57,7 @@ Add a `@moduledoc` to `Assets.Cache` that explains:
### Rationale
Explicit invalidation isn't needed because:
1. Assets are content-addressable (SHA256) and immutable — updates create new hashes, so old entries are never requested again
2. Periodic pruning (every 12h) cleans up stale entries
3. ETS table is in-memory, cleared on restart
@@ -3,8 +3,8 @@ id: ML-152
title: Add API version prefix to collection endpoints
status: Done
assignee: []
created_date: '2026-04-30 10:48'
updated_date: '2026-04-30 10:57'
created_date: "2026-04-30 10:48"
updated_date: "2026-04-30 10:57"
labels:
- api
- versioning
@@ -19,9 +19,11 @@ priority: medium
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
The JSON API endpoints under `/api/collection/*` (`lib/music_library_web/controllers/collection_controller.ex`) have no versioning prefix. If the response shape changes, external consumers would break without warning.
Add a `/api/v1/` prefix to all collection API routes:
- `/api/v1/collection`
- `/api/v1/collection/latest`
- `/api/v1/collection/random`
@@ -30,6 +32,7 @@ Add a `/api/v1/` prefix to all collection API routes:
Keep existing routes as deprecated redirects (301) for backward compatibility, or remove them if there are no known consumers.
Update:
- `lib/music_library_web/router.ex` (route definitions)
- Controller and JSON tests
- `test/prod.hurl` (post-deploy verification that uses API endpoints)
@@ -37,7 +40,9 @@ Update:
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 All collection API routes live under `/api/v1/collection/*`
- [x] #2 Existing `/api/collection/*` routes are removed or redirect to v1 with 301
- [x] #3 Controller and JSON tests pass against the new `/api/v1/` paths
@@ -48,9 +53,11 @@ Update:
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
## Implementation Plan
**Scope**: Version ALL routes under `/api` to `/api/v1/`, not just collection endpoints. Affected routes:
- `/api/v1/collection`
- `/api/v1/collection/latest`
- `/api/v1/collection/random`
@@ -61,6 +68,7 @@ Update:
**Decision**: Remove old routes (no 301 redirects) — user controls all consumers.
**Files to change**:
1. `lib/music_library_web/router.ex` — Move all routes from `/api` to `/api/v1`
2. `test/music_library_web/controllers/collection_controller_test.exs` — Update paths to `/api/v1/collection/*`
3. `test/prod.hurl` — Update `/api/collection/latest``/api/v1/collection/latest`
@@ -71,6 +79,7 @@ Update:
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
## Implementation complete
### Files changed
@@ -86,9 +95,11 @@ Update:
5. **`docs/architecture.md`** — Updated ArchiveController, AssetController, and CollectionController route entries to `/api/v1/...`.
### Test results
```
CollectionControllerTest: 5 passed
AssetControllerTest: 9 passed
ArchiveControllerTest: 1 passed
```
<!-- SECTION:NOTES:END -->
@@ -3,8 +3,8 @@ id: ML-153
title: Move MusicBrainz data transformations out of Record schema
status: Done
assignee: []
created_date: '2026-04-30 10:48'
updated_date: '2026-04-30 12:20'
created_date: "2026-04-30 10:48"
updated_date: "2026-04-30 12:20"
labels:
- refactor
- records
@@ -19,6 +19,7 @@ priority: medium
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
The `Record` schema (`lib/music_library/records/record.ex`) contains MusicBrainz-specific data transformation functions that belong in the MusicBrainz API layer, not in the database schema:
- `parse_artists/1` — extracts artist credits from MusicBrainz API response
@@ -30,13 +31,16 @@ These functions are called by `add_musicbrainz_data/2` (changeset pipeline) and
Move the parsing functions to appropriate `MusicBrainz` modules (e.g., `MusicBrainz.ReleaseGroup` already has related helpers) and keep only struct introspection/presentation functions on `Record` (`artist_names/1`, `releases/1`, `released?/1`, `format_release_date/1`, etc.).
Update callers in:
- `Record.changeset/2` and `add_musicbrainz_data/2`
- `Records` context (import functions)
- Any tests that reference the moved functions directly
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 `parse_artists/1`, `parse_subtype/2`, `parse_secondary_types/1`, and `attrs_from_release_group/1` are moved to `MusicBrainz` modules
- [x] #2 `Record` schema retains only struct introspection and presentation helpers
- [x] #3 All callers are updated to use the new locations
@@ -47,11 +51,15 @@ Update callers in:
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
`parse_artist_credits/1` and `parse_record_type/2` moved to `MusicBrainz.ReleaseGroup` — these are pure MusicBrainz data parsing functions with no Record schema knowledge. `attrs_from_release_group/1` stays on `Record` because it maps MusicBrainz response fields to Record changeset keys (an implicit Record schema dependency). The Record schema now delegates parsing to `ReleaseGroup.parse_artist_credits/1` and `ReleaseGroup.parse_record_type/2`, keeping the coupling boundary clean: MusicBrainz modules parse MusicBrainz data; the Record schema owns its own field mapping.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Moved `parse_artist_credits/1` and `parse_record_type/2` to `MusicBrainz.ReleaseGroup`. `attrs_from_release_group/1` kept on `Record` to avoid reverse dependency — it now delegates parsing to the new MusicBrainz functions. `Record.update_artists/1` uses `ReleaseGroup.parse_artist_credits/1`. All 884 tests pass.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -3,8 +3,8 @@ id: ML-154
title: Add early platform detection in runtime.exs for SQLite extensions
status: Done
assignee: []
created_date: '2026-04-30 10:48'
updated_date: '2026-04-30 12:37'
created_date: "2026-04-30 10:48"
updated_date: "2026-04-30 12:37"
labels:
- robustness
- sqlite
@@ -18,6 +18,7 @@ priority: low
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
`MusicLibrary.Repo.extension_path/1` raises `"Unsupported OS or platform"` when run on an unrecognised OS/architecture combination. This is called at startup via `config/runtime.exs` to resolve paths for `unicode` and `vec0` SQLite extensions.
Instead of silently degrading (which would cause confusing downstream failures), add early detection:
@@ -27,10 +28,13 @@ Instead of silently degrading (which would cause confusing downstream failures),
3. If unsupported, raise with a helpful message listing supported platforms and the detected OS/arch
The existing `raise` in `extension_path/1` stays as a defensive fallback (should not be reached if runtime.exs catches it first).
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 #1 `Repo.supported_platform?/0` returns true/false based on OS/arch
- [x] #2 #2 `config/runtime.exs` checks `supported_platform?/0` before loading extensions and raises with a helpful message on unsupported platforms
- [x] #3 #3 Existing supported platforms (darwin-amd64, darwin-arm64, linux-amd64, linux-arm64) continue to load extensions correctly
@@ -3,8 +3,8 @@ id: ML-155
title: Compact collection summary format + statistical preamble for chat context
status: Done
assignee: []
created_date: '2026-05-01 21:43'
updated_date: '2026-05-01 21:49'
created_date: "2026-05-01 21:43"
updated_date: "2026-05-01 21:49"
labels: []
dependencies: []
priority: high
@@ -13,7 +13,9 @@ priority: high
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Reduce the token count of collection_summary/0 by:
1. Compacting format_group/1 (year-only dates, remove type field, reduce max genres 3→2)
2. Adding statistical preamble (genre/formats/decade distribution) computed inline from fetched records
3. Updating tests for the new format
@@ -22,5 +24,7 @@ Reduce the token count of collection_summary/0 by:
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Compacted collection_summary format_group/1: year-only dates, removed type field, reduced max genres 3→2. Added statistical preamble (genre/formats/eras distribution + artist count) computed in-memory from already-fetched records. ~27% token reduction (~26.4k → ~19.4k for 1200 records). All 886 tests pass.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -3,22 +3,26 @@ id: ML-157
title: Prevent pi from accessing sensitive files
status: Done
assignee: []
created_date: '2026-05-03 13:30'
updated_date: '2026-05-03 14:50'
created_date: "2026-05-03 13:30"
updated_date: "2026-05-03 14:50"
labels: []
dependencies: []
references:
- 'backlog://document/doc-2'
- "backlog://document/doc-2"
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
To prevent the pi harness from accidentally reading and sending sensitive data to the LLM, we need a declarative way to intercept problematic commands that interact with sensitive files that for example contain secrets.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Pi cannot read `.env` files via the `read` tool — access is blocked with a notification in interactive mode
- [x] #2 Pi cannot read files matching `*secret*` or `*credential*` patterns via the `read` tool
- [x] #3 Pi cannot `cat .env` or `grep` inside `.ssh/` or `.aws/` via the `bash` tool
@@ -31,6 +35,7 @@ To prevent the pi harness from accidentally reading and sending sensitive data t
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
## Implementation Plan
### 1. Objective Alignment
@@ -44,6 +49,7 @@ The solution: a pi extension that intercepts `tool_call` events before execution
**Chosen: Route A — `tool_call` event interception with JSON config**
This is the simplest viable approach:
- ~80 lines of TypeScript + a JSON config file
- Uses pi's built-in `tool_call` event hook (no tool reimplementation)
- Inherits built-in rendering and behavior for all tools
@@ -88,15 +94,12 @@ Example config:
".gnupg/",
"~/.pi/agent/sessions/"
],
"blocked_commands": [
"printenv",
"\\benv\\b",
"\\bset\\b"
]
"blocked_commands": ["printenv", "\\benv\\b", "\\bset\\b"]
}
```
Initial patterns cover:
- `.env` files: `.env`, `.envrc` (`.env.*` is NOT included — it would false-positive on `.env.example`, which must remain readable)
- Key files: `*.pem`, `*.key`, `*.key.pub`
- Secret/credential files: paths containing `secret`, `credential`, `credentials`
@@ -113,6 +116,7 @@ Verification: File exists at `.pi/sensitive-paths.json` and is valid JSON.
File: `.pi/extensions/sensitive-file-guard.ts`
Implementation:
1. Read `.pi/sensitive-paths.json` at extension load time (synchronous, using `readFileSync`)
2. Compile `blocked_paths` entries into a regex matcher (glob → regex conversion, all patterns compiled with the case-insensitive `i` flag to handle case-insensitive filesystems like macOS):
- `*``.*`
@@ -127,7 +131,7 @@ Implementation:
b. Check the resolved absolute path against the compiled blocked path regexes.
- **Bash tool**: Two separate checks with distinct purposes:
a. **Blocked path scan:** Search the raw `event.input.command` string for fragments that match blocked path patterns (e.g., `.env`, `.ssh/`, `.aws/`). This catches commands like `cat .env`, `grep foo ~/.ssh/config`, `cat /absolute/path/.env`. The scan is a simple substring/regex match against the command text — it does not parse shell syntax. This covers the most common accidental access patterns.
b. **Blocked command check:** Test `event.input.command` against `blocked_commands` regexes (from config). These cover commands that leak secrets *without* a path argument in the command string (e.g., `printenv`, `env`, `set`, `export`). Each regex uses `\b` word-boundary anchors to avoid false positives (e.g., `env` should match `env` but not `environment`).
b. **Blocked command check:** Test `event.input.command` against `blocked_commands` regexes (from config). These cover commands that leak secrets _without_ a path argument in the command string (e.g., `printenv`, `env`, `set`, `export`). Each regex uses `\b` word-boundary anchors to avoid false positives (e.g., `env` should match `env` but not `environment`).
- **Blocking and notification:**
- If match found and `ctx.hasUI` is true: show a warning notification via `ctx.ui.notify()`
- If match found: return `{ block: true, reason: "Blocked sensitive path: <path or command>" }` (error message for non-interactive mode)
@@ -135,25 +139,26 @@ Implementation:
**Edge Cases and Accepted Limitations:**
| Scenario | Handling | Rationale |
|----------|----------|-----------|
| `cat ".env"` (quoted) | Caught by path substring scan | Quotes don't hide the path fragment |
| `cat ./.env` (relative with `./`) | Caught by path substring scan | `.env` fragment still present |
| `cat /absolute/path/.env` | Caught by path substring scan | `.env` fragment still present |
| `cat $HOME/project/.env` | Caught (if `.env` appears verbatim) | Variable expansion at start doesn't mask the path fragment |
| `head .env`, `tail .env`, `less .env` | Caught by path substring scan | All contain `.env` fragment |
| `echo $MY_SECRET` (no file access) | NOT caught | This reads from environment memory, not disk. PI would need separate treatment for this (out of scope) |
| `cat $(echo .env)` (command substitution) | NOT caught | Requires shell parsing; accepted limitation for accidental-access use case |
| `eval 'cat .env'` | NOT caught | Requires shell parsing; accepted limitation |
| `sh -c 'cat .env'` | Caught by path substring scan | `.env` appears in the command string |
| Case variants (`.ENV`, `.Env`) | Caught by case-insensitive regex (`i` flag) | macOS filesystems are case-insensitive |
| Path traversal (`../.env`) | Caught by `path.resolve` + `path.normalize` | Resolved absolute path contains `.env` fragment |
| Symlinks (`config -> ~/.aws/`) | NOT caught in initial implementation | Resolving symlinks requires I/O (`realpathSync`) per tool call. Documented limitation; users must not symlink sensitive directories into the project tree. Can be added later via a config option `followSymlinks: true` |
| `/etc/passwd` (system file) | NOT caught unless pattern added to config | Config is the source of truth for blocked paths; system files are not blocked by default |
| Scenario | Handling | Rationale |
| ----------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `cat ".env"` (quoted) | Caught by path substring scan | Quotes don't hide the path fragment |
| `cat ./.env` (relative with `./`) | Caught by path substring scan | `.env` fragment still present |
| `cat /absolute/path/.env` | Caught by path substring scan | `.env` fragment still present |
| `cat $HOME/project/.env` | Caught (if `.env` appears verbatim) | Variable expansion at start doesn't mask the path fragment |
| `head .env`, `tail .env`, `less .env` | Caught by path substring scan | All contain `.env` fragment |
| `echo $MY_SECRET` (no file access) | NOT caught | This reads from environment memory, not disk. PI would need separate treatment for this (out of scope) |
| `cat $(echo .env)` (command substitution) | NOT caught | Requires shell parsing; accepted limitation for accidental-access use case |
| `eval 'cat .env'` | NOT caught | Requires shell parsing; accepted limitation |
| `sh -c 'cat .env'` | Caught by path substring scan | `.env` appears in the command string |
| Case variants (`.ENV`, `.Env`) | Caught by case-insensitive regex (`i` flag) | macOS filesystems are case-insensitive |
| Path traversal (`../.env`) | Caught by `path.resolve` + `path.normalize` | Resolved absolute path contains `.env` fragment |
| Symlinks (`config -> ~/.aws/`) | NOT caught in initial implementation | Resolving symlinks requires I/O (`realpathSync`) per tool call. Documented limitation; users must not symlink sensitive directories into the project tree. Can be added later via a config option `followSymlinks: true` |
| `/etc/passwd` (system file) | NOT caught unless pattern added to config | Config is the source of truth for blocked paths; system files are not blocked by default |
**Bash scanning scope:** The bash scanner uses simple substring/regex matching on the raw command string. It is designed to catch *accidental* access by the LLM, not adversarial bypass by a human. Commands using shell variable expansion, command substitution, or indirect execution may evade detection; this is an accepted limitation documented above.
**Bash scanning scope:** The bash scanner uses simple substring/regex matching on the raw command string. It is designed to catch _accidental_ access by the LLM, not adversarial bypass by a human. Commands using shell variable expansion, command substitution, or indirect execution may evade detection; this is an accepted limitation documented above.
Verification:
- Start pi in this project, ask it to read `.env.example` (should work — this is NOT a secret, it's the example)
- Ask pi to read `.env` (should be blocked with a notification)
- Ask pi to run `cat .env` via bash (should be blocked)
@@ -211,6 +216,7 @@ Verification: All tests pass before proceeding to integration verification.
The extension must handle `ctx.hasUI === false` (print mode, JSON mode) by returning a descriptive error reason string, so the blocked access is reported to stdout rather than silently swallowed.
Verification:
```bash
pi -p "read the .env file" 2>&1 | grep -i "blocked"
# Should produce output indicating the access was blocked
@@ -219,6 +225,7 @@ pi -p "read the .env file" 2>&1 | grep -i "blocked"
**Step 4: Verify other tools are unaffected**
Ask pi to:
- Read a normal source file (e.g., `lib/music_library.ex`)
- Edit a normal file
- Run `mix test`
@@ -232,41 +239,41 @@ Verification: All normal pi operations are unaffected.
Each step includes concrete verification instructions above. Overall verification suite:
| Test Case | Tool | Target | Expected |
|-----------|------|--------|----------|
| Read .env | read | `.env` | Blocked |
| Read .env via path traversal | read | `../.env` | Blocked |
| Read .env.production | read | `.env.production` | Blocked (if pattern added) |
| Read .env.example | read | `.env.example` | **Allowed** (example file) |
| Read .ENV (case variant) | read | `.ENV` | Blocked (case-insensitive) |
| Read secrets file | read | `config/secrets.yml` | Blocked |
| Cat .env via bash | bash | `cat .env` | Blocked |
| Cat .env via bash with quotes | bash | `cat ".env"` | Blocked |
| Cat ./.env via bash | bash | `cat ./.env` | Blocked |
| Grep in .ssh | grep | `~/.ssh/config` | Blocked |
| Ls in .aws | ls | `~/.aws/` | Blocked |
| Write to .env | write | `.env` | Blocked |
| Edit .env | edit | `.env` | Blocked |
| Bash printenv (no path) | bash | `printenv` | Blocked |
| Bash env command | bash | `env` | Blocked |
| Bash echo $NODE_ENV (false positive check) | bash | `echo $NODE_ENV` | **Allowed** (`\b` anchor) |
| Read normal .ex file | read | `lib/music_library.ex` | Allowed |
| Run mix test | bash | `mix test` | Allowed |
| Non-interactive read .env | read | `.env` (print mode) | Error reported |
| Non-interactive read source file | read | `lib/music_library.ex` (print mode) | Normal output |
| Add pattern + /reload | read | newly blocked path | Blocked after reload |
| Test Case | Tool | Target | Expected |
| ------------------------------------------ | ----- | ----------------------------------- | -------------------------- |
| Read .env | read | `.env` | Blocked |
| Read .env via path traversal | read | `../.env` | Blocked |
| Read .env.production | read | `.env.production` | Blocked (if pattern added) |
| Read .env.example | read | `.env.example` | **Allowed** (example file) |
| Read .ENV (case variant) | read | `.ENV` | Blocked (case-insensitive) |
| Read secrets file | read | `config/secrets.yml` | Blocked |
| Cat .env via bash | bash | `cat .env` | Blocked |
| Cat .env via bash with quotes | bash | `cat ".env"` | Blocked |
| Cat ./.env via bash | bash | `cat ./.env` | Blocked |
| Grep in .ssh | grep | `~/.ssh/config` | Blocked |
| Ls in .aws | ls | `~/.aws/` | Blocked |
| Write to .env | write | `.env` | Blocked |
| Edit .env | edit | `.env` | Blocked |
| Bash printenv (no path) | bash | `printenv` | Blocked |
| Bash env command | bash | `env` | Blocked |
| Bash echo $NODE_ENV (false positive check) | bash | `echo $NODE_ENV` | **Allowed** (`\b` anchor) |
| Read normal .ex file | read | `lib/music_library.ex` | Allowed |
| Run mix test | bash | `mix test` | Allowed |
| Non-interactive read .env | read | `.env` (print mode) | Error reported |
| Non-interactive read source file | read | `lib/music_library.ex` (print mode) | Normal output |
| Add pattern + /reload | read | newly blocked path | Blocked after reload |
### 5. Architecture Impact Analysis
| Touchpoint | Impact |
|------------|--------|
| Touchpoint | Impact |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `.pi/extensions/sensitive-file-guard.ts` | **New file** — extension entry point. Named `000-sensitive-file-guard.ts` (or similar numeric prefix) to ensure it loads **first** among project-local extensions. Since `tool_call` handlers chain in load order and later handlers can mutate `event.input` before this guard sees it, loading first guarantees the guard inspects the original, unmodified tool arguments. |
| `.pi/sensitive-paths.json` | **New file** — declarative blocked patterns config |
| `tool_call` event | New subscriber, chains with existing extensions (e.g., MCP adapter). The guard runs **before** the MCP adapter's `tool_call` handler, so sensitive file access is blocked before any MCP forwarding occurs. |
| Built-in tools | **Unchanged** — interception is non-invasive |
| Phoenix/Ecto/Oban | **No impact** — pi-only concern, not part of the Elixir application |
| Production infrastructure | **No impact** — this is developer-local tooling, not server configuration |
| Other pi extensions (MCP adapter) | Compatible — `tool_call` handlers chain in load order. The guard runs alongside, not instead of. |
| `.pi/sensitive-paths.json` | **New file** — declarative blocked patterns config |
| `tool_call` event | New subscriber, chains with existing extensions (e.g., MCP adapter). The guard runs **before** the MCP adapter's `tool_call` handler, so sensitive file access is blocked before any MCP forwarding occurs. |
| Built-in tools | **Unchanged** — interception is non-invasive |
| Phoenix/Ecto/Oban | **No impact** — pi-only concern, not part of the Elixir application |
| Production infrastructure | **No impact** — this is developer-local tooling, not server configuration |
| Other pi extensions (MCP adapter) | Compatible — `tool_call` handlers chain in load order. The guard runs alongside, not instead of. |
**No migration or deprecation path needed** — this is a net-new capability.
@@ -283,6 +290,7 @@ Each step includes concrete verification instructions above. Overall verificatio
### 7. Benchmarking Requirements
**No benchmarks needed.** The guard is a simple gate with constant-time pattern matching:
- O(20) regex tests per tool call
- No I/O beyond the initial config read (done once at startup)
- No allocations beyond the return value object
@@ -293,6 +301,7 @@ If future patterns grow to hundreds of entries, a trie-based matcher could be co
### 8. Cost Profile
**Zero cost.** The guard:
- Uses no third-party APIs
- Consumes no paid resources
- Runs entirely locally in the pi process
@@ -314,6 +323,7 @@ If future patterns grow to hundreds of entries, a trie-based matcher could be co
### 10. Documentation Updates
**`docs/project-conventions.md`** — Add a section "Pi Security Configuration" documenting:
- The existence of the sensitive file guard extension
- The location and format of `.pi/sensitive-paths.json`
- How to add or remove blocked patterns
@@ -329,11 +339,13 @@ If future patterns grow to hundreds of entries, a trie-based matcher could be co
**SKILL.md (optional)** — Consider creating `.claude/skills/sensitive-file-guard/SKILL.md` so pi itself understands the guard's behavior. Without this, pi may try to read a blocked file, receive the block reason in the next turn, and need to re-plan. A SKILL.md pre-loads this knowledge so pi avoids attempting blocked paths proactively. This is a nice-to-have, not required for the initial implementation.
**No other documentation files need updates.** This is a pi-level concern, not an application architecture concern.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
## Implementation Notes
### Files Created
@@ -350,16 +362,19 @@ If future patterns grow to hundreds of entries, a trie-based matcher could be co
- **Fail-open**: If config file is missing or invalid, the extension silently does nothing.
### Key Limitation
- Does NOT resolve symlinks (would require `realpathSync` per tool call). Users must not symlink sensitive directories into the project tree.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Created two files:
1. **`.pi/sensitive-paths.json`** — Declarative config blocking `mise.local.toml` and other sensitive file patterns
2. **`.pi/extensions/000-sensitive-file-guard.ts`** — Extension that intercepts `tool_call` events before any file I/O occurs, blocks matching paths/commands, notifies in interactive mode, and reports error reason in non-interactive mode.
The guard covers all built-in file-access tools (read, grep, write, edit, find, ls, bash) and loads first (000- prefix) among project-local extensions to inspect original tool arguments.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -3,13 +3,13 @@ id: ML-158
title: Force production logs to single-line format
status: Done
assignee: []
created_date: '2026-05-03 13:51'
updated_date: '2026-05-04 13:43'
created_date: "2026-05-03 13:51"
updated_date: "2026-05-04 13:43"
labels:
- ready
dependencies: []
references:
- 'backlog://documents/doc-3'
- "backlog://documents/doc-3"
modified_files:
- mix.exs
- config/config.exs
@@ -25,11 +25,15 @@ priority: medium
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
When running in production, logs spanning multiple lines create issues — they cannot be easily filtered, and log output cannot be reversed reliably. We need to configure the `prod` environment to output logs on one line with appropriate metadata.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 HTTP request logs (GET /path + Sent 200) appear as a single logfmt line in production
- [x] #2 LiveView socket connection logs (CONNECTED TO Phoenix.LiveView.Socket) appear as a single line
- [x] #3 No log message in production output spans multiple physical lines — all newlines are escaped as \\n
@@ -41,6 +45,7 @@ When running in production, logs spanning multiple lines create issues — they
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
## Implementation Plan: Force production logs to single-line format
### Objective alignment
@@ -48,6 +53,7 @@ When running in production, logs spanning multiple lines create issues — they
The problem: multi-line log output in production prevents reliable line-based filtering and makes log output impossible to reverse. Log sources include HTTP request logs (two separate `Logger.info` calls from `Phoenix.Logger`), LiveView handshake logs (single `Logger.info` with embedded newlines), and any custom `Logger` calls that pass multi-line strings.
The solution has three layers:
1. **Logster** replaces `Phoenix.Logger` for HTTP request logging — merges `GET + Sent` into one logfmt line
2. **Custom telemetry handler** replaces `Phoenix.Logger`'s `[:phoenix, :socket_connected]` handler — flattens LiveView handshake into one line
3. **Custom Logger.Formatter** acts as a universal safety net — replaces any remaining embedded newlines with escaped `\n` in ALL log messages
@@ -72,16 +78,16 @@ Before writing code, verify two assumptions:
`Phoenix.Logger` auto-attaches to 8 telemetry events. With production log level `:info`, only these produce visible output:
| Event | Level | Multi-line? | Covered by |
|---|---|---|---|
| `[:phoenix, :endpoint, :start]` | `:info` | No (one line) | Logster v2 |
| `[:phoenix, :endpoint, :stop]` | `:info` | No (one line) | Logster v2 |
| `[:phoenix, :socket_connected]` | `:info` | **Yes** (4+ lines) | Custom handler (Step 4) |
| `[:phoenix, :error_rendered]` | `:error` | No (one line) | Silenced — acceptable (ErrorTracker already captures errors) |
| `[:phoenix, :router_dispatch, :start]` | `:debug` | Yes | Already filtered at `:info` level — no action needed |
| `[:phoenix, :socket_drain]` | `:debug` | No | Already filtered |
| `[:phoenix, :channel_joined]` | `:debug` | Yes | Already filtered (LiveView uses `"lv:"` topics not `"phoenix"` internal topics, but default log_join level is `:debug`) |
| `[:phoenix, :channel_handled_in]` | `:debug` | Yes | Already filtered |
| Event | Level | Multi-line? | Covered by |
| -------------------------------------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `[:phoenix, :endpoint, :start]` | `:info` | No (one line) | Logster v2 |
| `[:phoenix, :endpoint, :stop]` | `:info` | No (one line) | Logster v2 |
| `[:phoenix, :socket_connected]` | `:info` | **Yes** (4+ lines) | Custom handler (Step 4) |
| `[:phoenix, :error_rendered]` | `:error` | No (one line) | Silenced — acceptable (ErrorTracker already captures errors) |
| `[:phoenix, :router_dispatch, :start]` | `:debug` | Yes | Already filtered at `:info` level — no action needed |
| `[:phoenix, :socket_drain]` | `:debug` | No | Already filtered |
| `[:phoenix, :channel_joined]` | `:debug` | Yes | Already filtered (LiveView uses `"lv:"` topics not `"phoenix"` internal topics, but default log_join level is `:debug`) |
| `[:phoenix, :channel_handled_in]` | `:debug` | Yes | Already filtered |
**Verdict**: The only events that produce visible output at `:info`+ are endpoint start/stop, socket_connected, and error_rendered. Disabling Phoenix.Logger is safe — the first two are replaced by Logster, socket_connected is replaced by the custom handler, and losing error_rendered is acceptable because ErrorTracker already captures all errors via its own telemetry listener.
@@ -94,6 +100,7 @@ After adding the Logster dependency (Step 1), inspect `Logster`'s telemetry atta
**Verify**: In an IEx session: `Logster.__info__(:functions)` or check the Logster source to see which events it attaches to. If it covers `socket_connected`, **skip Step 4** — the custom telemetry handler is unnecessary.
#### Step 1: Add Logster dependency
- Add `{:logster, "~> 2.0.0-rc.5"}` to `mix.exs` deps
- Run `mix deps.get`
- **Verify**: `mix compile` succeeds, `Logster` module is available
@@ -222,18 +229,18 @@ After adding the Logster dependency (Step 1), inspect `Logster`'s telemetry atta
### Verifiability
| Step | Verification |
|---|---|
| 0a | Inspect stdout with `MIX_ENV=prod mix phx.server` before any changes — confirm only endpoint start/stop, socket_connected, and error_rendered appear at `:info`+ |
| 0b | Inspect Logster source in `deps/logster/` — check if `socket_connected` is handled; if yes, skip Step 4 |
| 1 | `mix compile` passes, `Logster` module is available in IEx |
| 2-6 | Start app with `MIX_ENV=prod mix phx.server`, hit endpoints, inspect stdout — all logs on one line |
| 4 | Visit a LiveView page (`/collection`) in prod mode, verify single-line output in stdout |
| 5 | `mix test test/music_library/logger/single_line_formatter_test.exs` — all tests pass |
| 7 | Both `rg` searches (escaped `\n` and multi-line Logger calls) return empty for `lib/` |
| 8 | `mix test` passes with ≥75% coverage; dev env test confirms multi-line output unchanged |
| 9 | `MIX_ENV=prod mix release` succeeds; release binary starts without errors; log output is single-line |
| 10 | Read both docs, confirm accuracy against implemented config and modules |
| Step | Verification |
| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0a | Inspect stdout with `MIX_ENV=prod mix phx.server` before any changes — confirm only endpoint start/stop, socket_connected, and error_rendered appear at `:info`+ |
| 0b | Inspect Logster source in `deps/logster/` — check if `socket_connected` is handled; if yes, skip Step 4 |
| 1 | `mix compile` passes, `Logster` module is available in IEx |
| 2-6 | Start app with `MIX_ENV=prod mix phx.server`, hit endpoints, inspect stdout — all logs on one line |
| 4 | Visit a LiveView page (`/collection`) in prod mode, verify single-line output in stdout |
| 5 | `mix test test/music_library/logger/single_line_formatter_test.exs` — all tests pass |
| 7 | Both `rg` searches (escaped `\n` and multi-line Logger calls) return empty for `lib/` |
| 8 | `mix test` passes with ≥75% coverage; dev env test confirms multi-line output unchanged |
| 9 | `MIX_ENV=prod mix release` succeeds; release binary starts without errors; log output is single-line |
| 10 | Read both docs, confirm accuracy against implemented config and modules |
### Architecture impact analysis
@@ -252,16 +259,19 @@ After adding the Logster dependency (Step 1), inspect `Logster`'s telemetry atta
**UI components**: None affected.
**Config changes**:
- `config/config.exs`: add `config :music_library, :single_line_logging, false`
- `mix.exs`: new dependency `{:logster, "~> 2.0.0-rc.5"}`
- `config/prod.exs`: 4 additions (phoenix logger disable, single_line_logging flag, logster config, formatter config)
- `lib/music_library/application.ex`: 1 conditional block (Logster attach + optional telemetry handler attach)
**New modules**:
- `lib/music_library/logger/single_line_formatter.ex` — Logger.Formatter format/4 function; requires `@moduledoc` (Credo strict mode)
- `lib/music_library_web/telemetry/log_handler.ex` — Phoenix socket telemetry handler (conditional on Step 0b); requires `@moduledoc`
**New tests**:
- `test/music_library/logger/single_line_formatter_test.exs` — unit tests for the formatter (newline replacement, iolist handling, metadata preservation)
- `test/music_library_web/telemetry/log_handler_test.exs` — unit tests for the telemetry handler (single-line output, param filtering)
- Integration assertions in existing or new test files for dev config unchanged
@@ -273,6 +283,7 @@ After adding the Logster dependency (Step 1), inspect `Logster`'s telemetry atta
### Performance profile
**Runtime complexity**: O(1) per log event.
- Logster: string interpolation from telemetry metadata per HTTP request
- Custom telemetry: same — one function call per socket connection
- Custom formatter: one `IO.chardata_to_string/1` + `String.replace/3` per log event (message size bounded by Logger truncation)
@@ -288,13 +299,15 @@ After adding the Logster dependency (Step 1), inspect `Logster`'s telemetry atta
### Benchmarking requirements
None needed. The operations are:
- String replacement on logger-truncated messages (bounded by default 4KB or configurable)
- String interpolation from telemetry metadata (no IO, no computation)
These are trivially fast. If future log volume increases by orders of magnitude, the `Logger` overload protection (message dropping at >500/sec) will engage before our formatter becomes a bottleneck.
These are trivially fast. If future log volume increases by orders of magnitude, the `Logger` overload protection (message dropping at >500/sec) will engage before our formatter becomes a bottleneck.
### Cost profile
No paid resources consumed.
- Logster: MIT license, free
- No API calls, no compute, no storage costs
- No third-party services
@@ -314,26 +327,32 @@ No manual production changes required. All configuration is in `config/prod.exs`
**Rollout**: Standard deploy (push to main → GitHub Actions → Coolify). First deploy will be slower due to new dependency compilation (Logster). Subsequent deploys use cached Docker layer.
**Rollback**: Revert to previous commit. No data migration needed.
<!-- SECTION:PLAN:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
## Implementation Summary
### What was done
#### Step 0: Pre-implementation verification
- Confirmed Phoenix.Logger fires 4 events at `:info`+ level (endpoint start/stop, socket_connected, error_rendered) — matches plan assumptions
- Confirmed Logster v2 handles `[:phoenix, :socket_connected]` — **skipped Step 4** (custom telemetry handler unnecessary)
#### Step 1: Added Logster dependency
- `mix.exs`: `{:logster, "~> 2.0.0-rc.5"}`
#### Step 2: Disabled Phoenix.Logger in prod
- `config/prod.exs`: `config :phoenix, :logger, false`
#### Step 3: Configured Logster with environment-conditional attach
- `config/config.exs`: `config :music_library, :single_line_logging, false`
- `config/prod.exs`: `config :music_library, :single_line_logging, true`
- `config/prod.exs`: Logster config with `extra_fields: [:request_id]` and parameter filtering
@@ -342,27 +361,33 @@ No manual production changes required. All configuration is in `config/prod.exs`
#### Step 4: Skipped (Logster v2 handles `[:phoenix, :socket_connected]`)
#### Step 5-6: Created custom Logger.Formatter as safety net
- `lib/music_library/logger/single_line_formatter.ex`: Implements `format/4` callback, replaces embedded `\n` with escaped `\\n`, handles string/iolist/report messages, preserves metadata
- `config/prod.exs`: formatter configured as `{MusicLibrary.Logger.SingleLineFormatter, :format}` with `metadata: [:request_id]`
#### Step 7: Codebase audit
- Searched for Logger calls with embedded `\n` — none found
- Multi-line Logger calls in source all produce single-line output (string concatenation without actual newlines)
- Custom formatter handles any remaining cases from OTP/Elixir internals
#### Step 8: Tests
- `test/music_library/logger/single_line_formatter_test.exs`: 13 tests covering newline replacement, iolist handling, metadata preservation, single-line output, dev/test config verification
- All 931 project tests pass
#### Step 9: OTP release verification
- `MIX_ENV=prod mix release` builds successfully
- sys.config confirms: `phoenix logger: false`, `single_line_logging: true`, Logster config, custom formatter tuple, logster application included
#### Step 10: Documentation
- `docs/production-infrastructure.md`: Added "Logging" section under Monitoring & Observability
- `docs/architecture.md`: Added `MusicLibrary.Logger.SingleLineFormatter` to Business Logic Modules, Logster note under Supervision Tree, and Web Utility Modules note
### Files changed
- `mix.exs` — added logster dependency
- `config/config.exs` — added `single_line_logging: false`
- `config/prod.exs` — 4 config additions (phoenix logger disable, flag, logster, formatter)
@@ -3,8 +3,8 @@ id: ML-159
title: select and copy log lines from log browser
status: Done
assignee: []
created_date: '2026-05-03 21:05'
updated_date: '2026-05-03 21:34'
created_date: "2026-05-03 21:05"
updated_date: "2026-05-03 21:34"
labels:
- enhancement
- pi-extension
@@ -20,6 +20,7 @@ priority: medium
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
When using the `/prod-logs` extension (`.pi/extensions/prod-logs/index.ts`), add the ability to select and copy log lines:
1. **Copy line under cursor** — The log browser currently has a scroll offset but no cursor concept. Add a visual cursor (highlighted line). On a keypress (e.g., Enter), copy that line, exit the log browser, and place the copied text where the user can paste it.
@@ -27,10 +28,13 @@ When using the `/prod-logs` extension (`.pi/extensions/prod-logs/index.ts`), add
2. **Select and copy multiple contiguous lines** — Add a selection mechanism. The user starts a selection, toggles lines on/off, then completes the selection. Copied lines should be in ascending order (oldest first — reversed compared to the visual display which shows newest first). Exit the log browser afterward so the copied lines can be pasted.
The log browser currently uses `ctx.ui.custom<void>`. To return copied text, the return type should change to `string | null` (null = cancelled/closed without copy). After the custom UI resolves, the copied text should be placed somewhere the user can immediately use (e.g., set in the editor, or copied to system clipboard).
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Cursor line is visually highlighted with accent color and `> ` prefix
- [x] #2 `v` enters visual mode; highlighted range extends as cursor moves
- [x] #3 `Escape` exits visual mode and clears selection
@@ -46,6 +50,7 @@ The log browser currently uses `ctx.ui.custom<void>`. To return copied text, the
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
## Implementation Plan: Vim-style Visual Mode + setEditorText
### 1. Add cursor state to LogViewer
@@ -55,6 +60,7 @@ Add `cursorIndex` (absolute index into `this.lines`) and initialize to `0`. The
**Navigation model (vim-style):** All movement keys (`j`, `k`, `PgUp`, `PgDn`, `Home`, `End`, `g`, `G`) move `cursorIndex`. The `scrollOffset` auto-adjusts via a `clampViewport()` helper to keep the cursor within the visible range `[scrollOffset, scrollOffset + visibleHeight)`. This unifies normal and visual mode key handling — in visual mode, the same keys extend the selection by moving `cursorIndex` while `visualAnchor` stays fixed.
Add a `clampViewport()` private method:
- If `cursorIndex < scrollOffset`, set `scrollOffset = cursorIndex`
- If `cursorIndex >= scrollOffset + visibleHeight`, set `scrollOffset = cursorIndex - visibleHeight + 1`
- Call `clampViewport()` after every cursor movement and after `updateLines()`
@@ -74,9 +80,9 @@ In visual mode, all movement keys (`j`, `k`, `PgUp`, `PgDn`, `Home`, `End`, `g`,
### 3. Update render to show cursor highlight and selection range
- Cursor line: `> ` prefix with `theme.fg("accent", ...)`
- Selected lines (in range): `● ` prefix with `theme.fg("success", ...)`
- Selected lines (in range): `● ` prefix with `theme.fg("success", ...)`
- Lines that are both cursor AND in selection: `> ` prefix takes priority (cursor indicator)
- Non-cursor, non-selected: keep existing ` NNN │ ` prefix
- Non-cursor, non-selected: keep existing `NNN │` prefix
The line number column must shift right by 2 characters to accommodate the new prefix: ` NNN │ > text` instead of ` NNN │ text`.
@@ -93,11 +99,13 @@ The line number column must shift right by 2 characters to accommodate the new p
### 5. Change ctx.ui.custom return type from void to string | null
```typescript
const copiedText = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
viewer.onCopy = (text: string) => done(text);
viewer.onClose = () => done(null);
// ...
});
const copiedText = await ctx.ui.custom<string | null>(
(tui, theme, _kb, done) => {
viewer.onCopy = (text: string) => done(text);
viewer.onClose = () => done(null);
// ...
},
);
if (copiedText !== null) {
ctx.ui.setEditorText(copiedText);
@@ -130,16 +138,17 @@ This is a **single-file, frontend-only change** to `.pi/extensions/prod-logs/ind
### Touchpoints
| Component | Impact |
|-----------|--------|
| `LogViewer` class | **Modified** — adds `cursorIndex`, `visualMode`, `visualAnchor`, `onCopy` callback, `clampViewport()` private method. Movement keys refactored to move `cursorIndex` with viewport auto-clamping. `handleInput` routing restructured for mode awareness |
| `LogViewer.render()` | **Modified** — adds cursor prefix (`> `) and selection prefix (`● `). Line number column padding widened by 2 chars |
| `LogViewer.updateLines()` | **Modified** — resets `cursorIndex = 0`, `visualMode = false`, `visualAnchor = 0`, calls `clampViewport()` |
| Extension handler function | **Modified**`ctx.ui.custom<void>``ctx.ui.custom<string \| null>`. Adds `onCopy` handler. Adds `ctx.ui.setEditorText()` on copy |
| Help text strings | **Modified** — split into normal-mode and visual-mode variants |
| `fetchLogs` / Coolify API | **No change** — data fetching unchanged |
| Component | Impact |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LogViewer` class | **Modified** — adds `cursorIndex`, `visualMode`, `visualAnchor`, `onCopy` callback, `clampViewport()` private method. Movement keys refactored to move `cursorIndex` with viewport auto-clamping. `handleInput` routing restructured for mode awareness |
| `LogViewer.render()` | **Modified** — adds cursor prefix (`> `) and selection prefix (`● `). Line number column padding widened by 2 chars |
| `LogViewer.updateLines()` | **Modified** — resets `cursorIndex = 0`, `visualMode = false`, `visualAnchor = 0`, calls `clampViewport()` |
| Extension handler function | **Modified**`ctx.ui.custom<void>``ctx.ui.custom<string \| null>`. Adds `onCopy` handler. Adds `ctx.ui.setEditorText()` on copy |
| Help text strings | **Modified** — split into normal-mode and visual-mode variants |
| `fetchLogs` / Coolify API | **No change** — data fetching unchanged |
### Deprecation/Migration
None. Old behavior (scroll + refresh + close) is preserved and extended, not replaced.
---
@@ -174,18 +183,20 @@ None. Old behavior (scroll + refresh + close) is preserved and extended, not rep
## Documentation Updates
| File | Change Needed |
|------|--------------|
| `docs/architecture.md` | **No change** — this is a pi extension, not part of the Elixir application architecture |
| `docs/project-conventions.md` | **No change** — TypeScript conventions are pi's domain, not the project's |
| `docs/production-infrastructure.md` | **No change** — no infra changes |
| `docs/available-tasks.md` | **No change** — no new mise tasks |
| File | Change Needed |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `docs/architecture.md` | **No change** — this is a pi extension, not part of the Elixir application architecture |
| `docs/project-conventions.md` | **No change** — TypeScript conventions are pi's domain, not the project's |
| `docs/production-infrastructure.md` | **No change** — no infra changes |
| `docs/available-tasks.md` | **No change** — no new mise tasks |
| `.pi/extensions/prod-logs/index.ts` | **Inline comments** — add JSDoc on new fields (`cursorIndex`, `visualMode`, `visualAnchor`, `onCopy`) and new key binding branches |
<!-- SECTION:PLAN:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Added vim-style cursor navigation and visual mode to the prod-logs pi extension:
- **Cursor**: `cursorIndex` tracks the highlighted line; all movement keys (j/k/PgUp/PgDn/Home/End/g/G) move the cursor with viewport auto-clamping via `clampViewport()`.
@@ -196,4 +207,5 @@ Added vim-style cursor navigation and visual mode to the prod-logs pi extension:
- **Help text**: Mode-aware — normal mode shows full key bindings, visual mode shows selection-specific keys.
Single-file change to `.pi/extensions/prod-logs/index.ts`. No Elixir code, database, or infrastructure changes. All 890 tests pass.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -3,8 +3,8 @@ id: ML-164
title: Create pi extension for interactive error browsing
status: Done
assignee: []
created_date: '2026-05-04 08:08'
updated_date: '2026-05-04 12:44'
created_date: "2026-05-04 08:08"
updated_date: "2026-05-04 12:44"
labels:
- pi
- ready
@@ -21,6 +21,7 @@ ordinal: 6000
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Build a pi extension that provides an interactive TUI for browsing production errors, using the `fetch_production_errors` and `fetch_production_error` tools from the parent task.
This extension gives the user (and LLM) a browseable interface for production errors, accessible via a slash command like `/prod-errors`.
@@ -78,11 +79,13 @@ This extension gives the user (and LLM) a browseable interface for production er
`.pi/extensions/prod-errors/index.ts` (new extension, separate from `prod-logs`)
The prod-logs extension already provides the `resolveVar` pattern and `fetchLogs` function. This extension follows the same conventions but for error_tracker data.
<!-- SECTION:DESCRIPTION:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
## Implementation Plan
### Objective alignment
@@ -106,14 +109,14 @@ Key fields used by the list view: `id`, `kind`, `reason`, `source_line`, `source
### Architecture impact analysis
| Touchpoint | Impact |
|---|---|
| `.pi/extensions/prod-errors/index.ts` | **New file** — ~400 lines: `ErrorBrowser` class, helpers, command registration |
| `.pi/extensions/prod-errors/package.json` | **New file** — minimal `{ name, private, description }` |
| `.pi/extensions/prod-logs/index.ts` | **No change** |
| All Elixir modules, router, PubSub, supervision tree | **No change** — purely a pi extension |
| Pi env vars | Reuses `PI_API_TOKEN` and `PI_SERVICE_FQDN_WEB` from ML-163 |
| Existing pi extensions | **No change**`/prod-errors` is a new command |
| Touchpoint | Impact |
| ---------------------------------------------------- | ------------------------------------------------------------------------------ |
| `.pi/extensions/prod-errors/index.ts` | **New file** — ~400 lines: `ErrorBrowser` class, helpers, command registration |
| `.pi/extensions/prod-errors/package.json` | **New file** — minimal `{ name, private, description }` |
| `.pi/extensions/prod-logs/index.ts` | **No change** |
| All Elixir modules, router, PubSub, supervision tree | **No change** — purely a pi extension |
| Pi env vars | Reuses `PI_API_TOKEN` and `PI_SERVICE_FQDN_WEB` from ML-163 |
| Existing pi extensions | **No change**`/prod-errors` is a new command |
### Performance profile
@@ -141,8 +144,13 @@ No paid resources. Makes HTTP requests to the project's own server. No third-par
#### Step 1: Create `.pi/extensions/prod-errors/package.json`
Minimal `package.json` matching the prod-logs pattern:
```json
{ "name": "prod-errors", "private": true, "description": "Interactive TUI for browsing production errors" }
{
"name": "prod-errors",
"private": true,
"description": "Interactive TUI for browsing production errors"
}
```
**Verification**: `ls -la .pi/extensions/prod-errors/package.json` — file must exist with valid JSON.
@@ -192,6 +200,7 @@ A class managing list view state and rendering, following the `LogViewer` patter
**Caching**: Cache rendered lines when `width` and state unchanged. Invalidate on any state mutation.
**Verification** (requires ML-162 API running locally):
1. `/reload``/prod-errors` → TUI opens with error list (or empty state).
2. `j`/`k` moves cursor. `r` toggles resolved. `m` toggles muted. `l` loads next page.
3. `Enter` triggers detail fetch. `q`/`Escape` closes TUI.
@@ -210,6 +219,7 @@ Extend `ErrorBrowser` with detail rendering. On Enter in list: set `mode = "load
**Loading state**: Inline "Loading…" overlay centered in viewport when `mode === "loading"`.
**Verification**:
1. Enter on error → detail shows all sections + occurrences with stacktraces.
2. `j`/`k` scrolls detail. `PgUp`/`PgDn` pages. `Escape` returns to list with cursor preserved.
3. Enter on detail line → line copied to editor (prod-logs pattern).
@@ -220,6 +230,7 @@ Extend `ErrorBrowser` with detail rendering. On Enter in list: set `mode = "load
#### Step 5: Register the `/prod-errors` command
Register via `pi.registerCommand("prod-errors", ...)`. Handler flow:
1. Validate `PI_SERVICE_FQDN_WEB` and `PI_API_TOKEN` — notify and return if missing.
2. Show `BorderedLoader` while calling `fetchErrors({ limit: 50, offset: 0 })`.
3. On null/empty: notify and return.
@@ -229,6 +240,7 @@ Register via `pi.registerCommand("prod-errors", ...)`. Handler flow:
In-browser fetches (filter toggles, load more, detail) use an inline loading indicator, not nested `BorderedLoader`.
**Verification**:
1. `/prod-errors` → loader → browser. Toggle filters → inline loading. Load more → inline loading. Detail → inline loading.
2. Bad `PI_SERVICE_FQDN_WEB` → useful error notification. All keyboard shortcuts work.
@@ -236,25 +248,26 @@ In-browser fetches (filter toggles, load more, detail) use an inline loading ind
#### Step 6: Edge cases and error handling
| Scenario | Handling |
|---|---|
| Empty list (API returned zero errors) | "No production errors found" in TUI |
| Scenario | Handling |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Empty list (API returned zero errors) | "No production errors found" in TUI |
| All errors filtered out by current toggles | "No errors match the current filters" (only shown when the unfiltered list was non-empty; track with a `totalUnfiltered` field set on the initial fetch) |
| API non-2xx | Error notification with status code; keep TUI open |
| Unexpected JSON shape | Catch TypeError, "Unexpected API response format" notification |
| Network timeout/refused | Catch fetch error, show notification |
| Double Enter while loading | Ignore (`mode === "loading"` guard) |
| Rapid filter toggles | Abort in-flight request via AbortController before new fetch |
| Filter changes invalidate pages | Reset offset to 0, replace errors with first page |
| Load more at end | Show "— end of results —", ignore `l` |
| Zero occurrences | "No occurrences recorded" |
| Empty context/breadcrumbs | Omit those sections |
| Missing stacktrace fields | Show "—" |
| Narrow terminal (< 40 cols) | Render with heavy truncation (acceptable) |
| API non-2xx | Error notification with status code; keep TUI open |
| Unexpected JSON shape | Catch TypeError, "Unexpected API response format" notification |
| Network timeout/refused | Catch fetch error, show notification |
| Double Enter while loading | Ignore (`mode === "loading"` guard) |
| Rapid filter toggles | Abort in-flight request via AbortController before new fetch |
| Filter changes invalidate pages | Reset offset to 0, replace errors with first page |
| Load more at end | Show "— end of results —", ignore `l` |
| Zero occurrences | "No occurrences recorded" |
| Empty context/breadcrumbs | Omit those sections |
| Missing stacktrace fields | Show "—" |
| Narrow terminal (< 40 cols) | Render with heavy truncation (acceptable) |
**Abort pattern**: Store `currentAbortController` reference; call `.abort()` before each new fetch; catch `AbortError` and return null.
**Verification**:
1. **Rapid filter toggle**: Press `r` 5 times rapidly → observe only the last fetch's results are displayed (previous requests were aborted). Requests aborted via `AbortController` should not update state.
2. **End-of-results**: Navigate to the last page with `l`, then press `l` again → observe "— end of results —" divider and subsequent `l` presses are ignored.
3. **Empty context/breadcrumbs**: View a detail for an error that has occurrences with `context: {}` and `breadcrumbs: []` → observe those section headings are entirely omitted from the detail rendering.
@@ -291,6 +304,7 @@ No new changes beyond ML-162/ML-163: `PI_API_TOKEN` and `PI_SERVICE_FQDN_WEB` ar
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
## Implementation Notes
### What was built
@@ -3,17 +3,18 @@ id: ML-17
title: Experimental comment left in Last.fm API client
status: Done
assignee: []
created_date: '2026-04-20 08:50'
created_date: "2026-04-20 08:50"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/162'
- "https://github.com/cloud8421/music_library/issues/162"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-08 · closed 2026-04-08_
## Summary
@@ -27,11 +28,14 @@ _GitHub: created 2026-04-05 · updated 2026-04-08 · closed 2026-04-08_
## Suggested Fix
Either:
1. Validate the timeout values work well and remove the "Experimental" comment
2. Or revert to standard timeouts if the experiment didn't prove useful
## Acceptance Criteria
<!-- AC:BEGIN -->
- No experimental/temporary comments remain in production code
- Timeout configuration is intentional and documented
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-2
title: Verify or drop multipart hex dependency
status: Done
assignee: []
created_date: '2026-04-20 08:44'
created_date: "2026-04-20 08:44"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/182'
- "https://github.com/cloud8421/music_library/issues/182"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-16 · updated 2026-04-16 · closed 2026-04-16_
## Summary
@@ -36,6 +37,8 @@ _GitHub: created 2026-04-16 · updated 2026-04-16 · closed 2026-04-16_
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 Decision recorded: either dep removed, or dep retained with comment explaining transitive need
<!-- AC:END -->
@@ -3,12 +3,12 @@ id: ML-21
title: Classify API errors as transient vs permanent in workers
status: Done
assignee: []
created_date: '2026-04-20 08:50'
updated_date: '2026-04-24 11:59'
created_date: "2026-04-20 08:50"
updated_date: "2026-04-24 11:59"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/158'
- "https://github.com/cloud8421/music_library/issues/158"
priority: medium
ordinal: 1000
---
@@ -16,6 +16,7 @@ ordinal: 1000
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-12 · re-scoped 2026-04-24 after per-API research_
## Summary
@@ -31,15 +32,15 @@ The original issue pointed at `LastFm.API.ErrorResponse` as the pattern to copy
## What each API actually returns
| API | Error channel | Rate limit status | Body shape | Retry hint |
|-----|---------------|-------------------|------------|------------|
| Last.fm | HTTP 200 + body | code `29` in body | `{"error": N, "message": "..."}` | none (already handled) |
| MusicBrainz | HTTP status | **503** (not 429) | `{"error": "string"}` | `Retry-After` |
| Discogs | HTTP status | 429 | `{"message": "..."}` | `X-Discogs-Ratelimit-*`, no `Retry-After` |
| Wikipedia REST v1 | HTTP status | 429 | `{"httpCode", "messageTranslations"}` | `Retry-After` |
| Wikipedia/Wikidata Action API | **HTTP 200 + body** (Last.fm-shaped) | 429 or 503 | `{"error": {"code", "info"}}` | `Retry-After` on 503 |
| Brave Search | HTTP status | 429 | `{"type": "ErrorResponse", "error": {"status", "code", "detail"}}` | `X-RateLimit-Reset` (seconds) |
| OpenAI | HTTP status + body `code` | 429 for **both** rate limit and quota | `{"error": {"type", "code", "message", "param"}}` | `x-ratelimit-reset-*` |
| API | Error channel | Rate limit status | Body shape | Retry hint |
| ----------------------------- | ------------------------------------ | ------------------------------------- | ------------------------------------------------------------------ | ----------------------------------------- |
| Last.fm | HTTP 200 + body | code `29` in body | `{"error": N, "message": "..."}` | none (already handled) |
| MusicBrainz | HTTP status | **503** (not 429) | `{"error": "string"}` | `Retry-After` |
| Discogs | HTTP status | 429 | `{"message": "..."}` | `X-Discogs-Ratelimit-*`, no `Retry-After` |
| Wikipedia REST v1 | HTTP status | 429 | `{"httpCode", "messageTranslations"}` | `Retry-After` |
| Wikipedia/Wikidata Action API | **HTTP 200 + body** (Last.fm-shaped) | 429 or 503 | `{"error": {"code", "info"}}` | `Retry-After` on 503 |
| Brave Search | HTTP status | 429 | `{"type": "ErrorResponse", "error": {"status", "code", "detail"}}` | `X-RateLimit-Reset` (seconds) |
| OpenAI | HTTP status + body `code` | 429 for **both** rate limit and quota | `{"error": {"type", "code", "message", "param"}}` | `x-ratelimit-reset-*` |
## Real quirks to encode (not 14 error atoms)
@@ -69,7 +70,9 @@ Out of scope (tracked separately): honouring `Retry-After` / `X-*-Reset` headers
<!-- AC:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 MusicBrainz, Discogs, Wikipedia REST v1, Brave Search, and OpenAI API modules expose a classifier that maps {status, body} to :retry or :cancel
- [x] #2 Wikipedia Action API responses (wbgetentities, prop=extracts) decode HTTP 200 bodies containing {"error": ...} into {:error, reason} instead of {:ok, body}
- [x] #3 OpenAI classifier distinguishes rate_limit_exceeded (retry) from insufficient_quota (cancel) despite both being HTTP 429
@@ -81,6 +84,7 @@ Out of scope (tracked separately): honouring `Retry-After` / `X-*-Reset` headers
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
## Summary
Introduced structured per-API `ErrorResponse` modules for MusicBrainz, Discogs, Wikipedia, Brave Search, and OpenAI, so workers can now distinguish transient failures (rate limit, 5xx, timeout) from permanent ones (4xx, not found, auth, quota). Workers emit `{:snooze, seconds}` / `{:cancel, reason}` / `{:error, reason}` instead of bubbling raw bodies. Preserved the existing `LastFm.API.ErrorResponse` behaviour and added struct-based `retryable?/1` / `retry_delay_seconds/1` helpers so Last.fm plugs into the same handler.
@@ -88,10 +92,12 @@ Introduced structured per-API `ErrorResponse` modules for MusicBrainz, Discogs,
## What changed
**New shared modules**
- `MusicLibrary.HttpError` — default HTTP status → kind mapping
- `MusicLibrary.Worker.ErrorHandler.to_oban_result/1` — maps any known `ErrorResponse` struct to the correct Oban tuple; passes through `{:ok, _}`, `{:cancel, _}`, and atom-reason errors unchanged
**New per-API `ErrorResponse` modules**
- `MusicBrainz.API.ErrorResponse` — maps 503 to `:rate_limit` (MusicBrainz-specific; 429 is not used upstream)
- `Discogs.API.ErrorResponse`
- `Wikipedia.API.ErrorResponse` — has a dedicated `from_action_api_body/1` for HTTP 200 Action API error envelopes (AC2 silent-bug fix)
@@ -99,10 +105,12 @@ Introduced structured per-API `ErrorResponse` modules for MusicBrainz, Discogs,
- `OpenAI.API.ErrorResponse` — disambiguates HTTP 429 between `rate_limit_exceeded` (retry) and `insufficient_quota` (cancel) via body `code` (AC3)
**Updated API modules** — each now attaches a `parse_error/1` Req response step that halts with the appropriate struct on failure:
- `MusicBrainz.API`, `Discogs.API`, `Wikipedia.API`, `BraveSearch.API`
- `OpenAI.API``gpt/2`, `get_embeddings/2`, and `chat_stream/6` now return `{:error, %OpenAI.API.ErrorResponse{}}` instead of `{:error, "OpenAI API error: " <> inspect(body)}` strings
**Updated workers** to route through `ErrorHandler.to_oban_result/1`, preserving existing atom-cancel branches (`:no_english_wikipedia`, `:cover_not_available`, `:image_not_found`, `:no_discogs_data`):
- `ArtistRefreshMusicBrainzData`, `ArtistRefreshDiscogsData`, `ArtistRefreshWikipediaData`
- `FetchArtistInfo`, `FetchArtistImage`, `FetchArtistLastFmData`
- `RefreshCover`, `RecordRefreshMusicBrainzData`
@@ -127,4 +135,5 @@ Full verification: `mise run dev:precommit` — shellcheck, credo --strict, sobe
## Out of scope (ML-146)
Honouring `Retry-After` / `X-*-Reset` headers to derive precise snooze durations — each ErrorResponse module currently returns fixed per-kind defaults (3060 s).
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -3,17 +3,18 @@ id: ML-22
title: Search + count function pairs duplicate query logic
status: Done
assignee: []
created_date: '2026-04-20 08:50'
created_date: "2026-04-20 08:50"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/157'
- "https://github.com/cloud8421/music_library/issues/157"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-09 · closed 2026-04-09_
## Summary
@@ -35,7 +36,9 @@ Multiple contexts have paired search/count functions that duplicate the same que
Extract the shared query builder into a private function that both the search and count functions use.
## Acceptance Criteria
<!-- AC:BEGIN -->
- Query logic is defined once per search domain
- No regression in search results or counts
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-24
title: RecordComponents is 755 lines with 40+ mixed functions
status: Done
assignee: []
created_date: '2026-04-20 08:51'
created_date: "2026-04-20 08:51"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/155'
- "https://github.com/cloud8421/music_library/issues/155"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-12 · closed 2026-04-12_
## Summary
@@ -33,12 +34,15 @@ _GitHub: created 2026-04-05 · updated 2026-04-12 · closed 2026-04-12_
## Suggested Fix
Split into focused component modules:
- `RecordComponents.Grid` / `RecordComponents.List` — display layouts
- `RecordComponents.Actions` — shared dropdown/action menus
- Keep `RecordComponents` for simple helpers and badges
## Acceptance Criteria
<!-- AC:BEGIN -->
- Each module has a clear, focused purpose
- Shared action menu markup lives in one place
- No regression in rendering
@@ -3,17 +3,18 @@ id: ML-25
title: recompact_positions issues per-row UPDATEs instead of bulk
status: Done
assignee: []
created_date: '2026-04-20 08:51'
created_date: "2026-04-20 08:51"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/154'
- "https://github.com/cloud8421/music_library/issues/154"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-06 · closed 2026-04-06_
## Summary
@@ -34,7 +35,9 @@ _GitHub: created 2026-04-05 · updated 2026-04-06 · closed 2026-04-06_
Rewrite `recompact_positions/1` to use a single `UPDATE ... CASE` statement, consistent with `reorder_records_in_set/2`.
## Acceptance Criteria
<!-- AC:BEGIN -->
- Position recompaction uses a single bulk UPDATE
- Positions are correctly reassigned with no gaps
- No regression in ordering behaviour
@@ -3,17 +3,18 @@ id: ML-29
title: Collection/Wishlist index LiveView duplication
status: Done
assignee: []
created_date: '2026-04-20 08:51'
created_date: "2026-04-20 08:51"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/150'
- "https://github.com/cloud8421/music_library/issues/150"
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-13 · closed 2026-04-13_
## Summary
@@ -36,7 +37,9 @@ _GitHub: created 2026-04-05 · updated 2026-04-13 · closed 2026-04-13_
Extract shared index behaviour into a `LiveHelpers` module or use a shared base pattern that both LiveViews delegate to, parameterizing only the differences (base query filter, routes, section name).
## Acceptance Criteria
<!-- AC:BEGIN -->
- Shared logic lives in one place
- Both index pages retain their current functionality
- Adding a new shared feature (e.g., new sort option) requires changes in one location
@@ -3,17 +3,18 @@ id: ML-3
title: Add tests for ArtistLive.Form and RecordSetLive.RecordPicker LiveComponents
status: Done
assignee: []
created_date: '2026-04-20 08:48'
created_date: "2026-04-20 08:48"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/180'
- "https://github.com/cloud8421/music_library/issues/180"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-16 · updated 2026-04-19 · closed 2026-04-19_
## Summary
@@ -45,7 +46,9 @@ Add `Phoenix.LiveViewTest`-based tests (needed because these are LiveComponents
Fixture: use existing `MusicLibrary.RecordsFixtures` and `MusicLibrary.ArtistInfoFixtures`.
## Acceptance Criteria
<!-- AC:BEGIN -->
- New test files at `test/music_library_web/live/artist_live/form_test.exs` and `test/music_library_web/live/record_set_live/record_picker_test.exs`
- Both components exercise happy paths and at least one error/empty state
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-30
title: Silent error suppression in Artists.refresh_lastfm_data/1
status: Done
assignee: []
created_date: '2026-04-20 08:51'
created_date: "2026-04-20 08:51"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/149'
- "https://github.com/cloud8421/music_library/issues/149"
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-05 · closed 2026-04-05_
## Summary
@@ -36,7 +37,9 @@ _GitHub: created 2026-04-05 · updated 2026-04-05 · closed 2026-04-05_
Follow the project's `best_effort_*` pattern: log a warning and return the unchanged struct, but make the suppression explicit and observable.
## Acceptance Criteria
<!-- AC:BEGIN -->
- Last.fm API failures are logged as warnings
- Callers can still proceed without the Last.fm data
- Monitoring/logs reflect when Last.fm enrichment fails
@@ -3,17 +3,18 @@ id: ML-31
title: Deeply nested subqueries in ListeningStats
status: Done
assignee: []
created_date: '2026-04-20 08:52'
created_date: "2026-04-20 08:52"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/148'
- "https://github.com/cloud8421/music_library/issues/148"
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-11 · closed 2026-04-11_
## Summary
@@ -23,6 +24,7 @@ _GitHub: created 2026-04-05 · updated 2026-04-11 · closed 2026-04-11_
## Why This Matters
Three query builders create multi-level nested subqueries:
- `tracks_with_record_info_query` (lines 344-363) — 3 left joins to subqueries containing their own subqueries
- `top_albums_base_query` (lines 365-388) — same nested pattern for album aggregation
- `top_artists_base_query` (lines 390-404) — third-order subqueries
@@ -42,7 +44,9 @@ Additionally, `unique_collected_releases_query` and `unique_wishlisted_releases_
- Deduplicate the `unique_*_releases_query` helpers
## Acceptance Criteria
<!-- AC:BEGIN -->
- Query plans for `list_tracks` and `recent_activity` show reduced nesting
- No regression in query results
- Benchmarks show comparable or better performance
@@ -3,17 +3,18 @@ id: ML-32
title: OpenAI API uses unsafe bang methods and lacks rate limiting
status: Done
assignee: []
created_date: '2026-04-20 08:52'
created_date: "2026-04-20 08:52"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/147'
- "https://github.com/cloud8421/music_library/issues/147"
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-04-05 · updated 2026-04-05 · closed 2026-04-05_
## Summary
@@ -39,7 +40,9 @@ The OpenAI API client is the only integration that uses `Req.post!()` (bang meth
3. Attach `Req.RateLimiter` in the request pipeline
## Acceptance Criteria
<!-- AC:BEGIN -->
- OpenAI API calls return `{:ok, _}` / `{:error, _}` tuples like all other integrations
- Rate limiter is attached with a configurable cooldown
- `OpenAI.Config` follows the same structure as other API Config modules
@@ -3,17 +3,18 @@ id: ML-33
title: Align Last.fm callback errors with app conventions
status: Done
assignee: []
created_date: '2026-04-20 08:52'
created_date: "2026-04-20 08:52"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/145'
- "https://github.com/cloud8421/music_library/issues/145"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-03-30 · updated 2026-03-30 · closed 2026-03-30_
## Summary
@@ -41,7 +42,9 @@ This is inconsistent with the documented convention for user-facing errors and i
Align the controller with the established convention: keep the contextual prefix, translate the reason through `ErrorMessages.friendly_message/1`, ensure the resulting string remains wrapped appropriately for localization/user display.
## Acceptance Criteria
<!-- AC:BEGIN -->
- The Last.fm callback uses the same user-facing error formatting approach as the rest of the app.
- Tests assert the controller does not expose raw backend error terms in the flash.
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-34
title: Make record creation resilient to color extraction failure
status: Done
assignee: []
created_date: '2026-04-20 08:52'
created_date: "2026-04-20 08:52"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/144'
- "https://github.com/cloud8421/music_library/issues/144"
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-03-30 · updated 2026-03-30 · closed 2026-03-30_
## Summary
@@ -41,7 +42,9 @@ This can leave partially-applied state: the record is committed, the caller sees
Refactor `create_record/1` so post-insert enrichment is failure-tolerant and consistent with the public contract. Reasonable options: treat color extraction as best-effort and continue without crashing; move the enrichment into an explicit background step; or wrap creation plus required follow-up in a transaction if failure must abort the whole operation.
## Acceptance Criteria
<!-- AC:BEGIN -->
- `create_record/1` never raises on color extraction failure.
- The function returns a documented result tuple for both success and failure paths.
- Tests cover a failed color extraction scenario.
@@ -3,17 +3,18 @@ id: ML-35
title: Harden the public asset endpoint against invalid payloads
status: Done
assignee: []
created_date: '2026-04-20 08:52'
created_date: "2026-04-20 08:52"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/143'
- "https://github.com/cloud8421/music_library/issues/143"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-03-30 · updated 2026-03-30 · closed 2026-03-30_
## Summary
@@ -42,7 +43,9 @@ This endpoint is public and cacheable. Invalid user input should degrade to a co
Handle invalid payloads and transform failures explicitly: replace bang-style decode with tuple-based validation in the controller boundary; convert failed image processing into a controlled 404/422/500 strategy; add regression tests for malformed payloads and failed conversion/resize paths.
## Acceptance Criteria
<!-- AC:BEGIN -->
- Invalid payloads do not raise from the controller.
- Failed image transforms do not crash the request path.
- Tests cover malformed payload and transform failure scenarios.
@@ -3,17 +3,18 @@ id: ML-36
title: Return controlled errors from genre population
status: Done
assignee: []
created_date: '2026-04-20 08:52'
created_date: "2026-04-20 08:52"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/142'
- "https://github.com/cloud8421/music_library/issues/142"
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-03-30 · updated 2026-03-30 · closed 2026-03-30_
## Summary
@@ -40,7 +41,9 @@ A transient OpenAI failure becomes an exception path instead of a controlled dom
Make `populate_genres/1` consistently return tagged tuples using `with {:ok, response} <- OpenAI.gpt(completion), {:ok, updated_record} <- Repo.update(...) do ... end`, mapping external API failures into a domain error shape the worker can handle intentionally.
## Acceptance Criteria
<!-- AC:BEGIN -->
- OpenAI/API failures do not raise from `populate_genres/1`.
- The worker behavior is explicit for retryable vs non-retryable failures.
- Tests cover an API failure path.
@@ -3,17 +3,18 @@ id: ML-37
title: Fix production migration strategy mismatch
status: Done
assignee: []
created_date: '2026-04-20 08:53'
created_date: "2026-04-20 08:53"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/141'
- "https://github.com/cloud8421/music_library/issues/141"
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-03-30 · updated 2026-03-30 · closed 2026-03-30_
## Summary
@@ -41,7 +42,9 @@ A deploy can start successfully against an outdated schema, creating a real risk
- `docs/production-infrastructure.md`
## Acceptance Criteria
<!-- AC:BEGIN -->
- Production deploys cannot start on an unmigrated schema.
- The documented migration strategy matches the implemented one.
- There is at least one automated check covering the chosen behavior.
@@ -3,17 +3,18 @@ id: ML-38
title: Inconsistent Logger style in workers (eager vs lazy)
status: Done
assignee: []
created_date: '2026-04-20 08:53'
created_date: "2026-04-20 08:53"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/139'
- "https://github.com/cloud8421/music_library/issues/139"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-03-25 · updated 2026-03-25 · closed 2026-03-25_
## Description
@@ -21,11 +22,13 @@ _GitHub: created 2026-03-25 · updated 2026-03-25 · closed 2026-03-25_
Workers use two different Logger styles:
**Lazy (preferred)** — `PruneAssetCache` (`lib/music_library/worker/prune_asset_cache.ex:12`):
```elixir
Logger.info(fn -> "Pruned #{prune_count} old cached assets..." end)
```
**Eager** — `PruneAssets` (`lib/music_library/worker/prune_assets.ex:34`):
```elixir
Logger.info("Pruned #{count} unreferenced assets.")
```
@@ -37,4 +40,5 @@ Standardize on lazy logging with `fn -> ... end` for efficiency (avoids string i
## Found during
Codebase consistency audit (2026-03-25)
<!-- SECTION:DESCRIPTION:END -->
@@ -3,17 +3,18 @@ id: ML-39
title: Missing dark mode class in search_components.ex
status: Done
assignee: []
created_date: '2026-04-20 08:53'
created_date: "2026-04-20 08:53"
labels: []
dependencies: []
references:
- 'https://github.com/cloud8421/music_library/issues/138'
- "https://github.com/cloud8421/music_library/issues/138"
priority: low
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
_GitHub: created 2026-03-25 · updated 2026-03-25 · closed 2026-03-25_
## Description
@@ -27,4 +28,5 @@ Add `dark:text-zinc-400` to the class list at line 51, consistent with the proje
## Found during
Codebase consistency audit (2026-03-25)
<!-- SECTION:DESCRIPTION:END -->

Some files were not shown because too many files have changed in this diff Show More