Run prettier on backlog folder
This commit is contained in:
@@ -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 -->
|
||||
|
||||
+4
-2
@@ -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 -->
|
||||
|
||||
+4
-1
@@ -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 -->
|
||||
|
||||
+5
-2
@@ -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 -->
|
||||
|
||||
+6
-2
@@ -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 -->
|
||||
|
||||
+5
-2
@@ -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 -->
|
||||
|
||||
+5
-2
@@ -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
|
||||
|
||||
+7
-3
@@ -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,17 +3,18 @@ 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
|
||||
@@ -21,7 +22,7 @@ _GitHub: created 2026-03-12 · updated 2026-04-09 · closed 2026-03-16 · not pl
|
||||
The four modules flagged in #99 remain at the same size:
|
||||
|
||||
| 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 |
|
||||
@@ -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 -->
|
||||
|
||||
+9
-3
@@ -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 -->
|
||||
|
||||
+4
-2
@@ -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 -->
|
||||
|
||||
+4
-2
@@ -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 -->
|
||||
|
||||
+6
-3
@@ -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
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
+4
-2
@@ -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 -->
|
||||
|
||||
+5
-2
@@ -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 -->
|
||||
|
||||
+5
-3
@@ -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 -->
|
||||
|
||||
+4
-2
@@ -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 -->
|
||||
|
||||
+5
-3
@@ -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 -->
|
||||
|
||||
+5
-3
@@ -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 -->
|
||||
|
||||
+5
-3
@@ -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 -->
|
||||
|
||||
@@ -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.
|
||||
|
||||
+4
-2
@@ -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 -->
|
||||
|
||||
+6
-3
@@ -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
|
||||
|
||||
+8
-2
@@ -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.
|
||||
|
||||
+12
-4
@@ -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,6 +16,7 @@ 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.
|
||||
@@ -23,7 +24,7 @@ Once workers classify API errors as transient vs permanent, the next refinement
|
||||
## 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 |
|
||||
@@ -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)
|
||||
|
||||
+8
-2
@@ -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 -->
|
||||
|
||||
+10
-2
@@ -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 -->
|
||||
|
||||
+6
-2
@@ -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
|
||||
|
||||
+6
-2
@@ -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)
|
||||
@@ -136,7 +140,7 @@ 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 |
|
||||
@@ -151,9 +155,10 @@ Implementation:
|
||||
| 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`
|
||||
@@ -233,7 +240,7 @@ 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) |
|
||||
@@ -259,7 +266,7 @@ Each step includes concrete verification instructions above. Overall verificatio
|
||||
### 5. Architecture Impact Analysis
|
||||
|
||||
| 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. |
|
||||
@@ -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
|
||||
@@ -73,7 +79,7 @@ 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) |
|
||||
@@ -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
|
||||
@@ -223,7 +230,7 @@ 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 |
|
||||
@@ -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()`
|
||||
@@ -76,7 +82,7 @@ In visual mode, all movement keys (`j`, `k`, `PgUp`, `PgDn`, `Home`, `End`, `g`,
|
||||
- Cursor line: `> ` prefix with `theme.fg("accent", ...)`
|
||||
- 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) => {
|
||||
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);
|
||||
@@ -131,7 +139,7 @@ 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()` |
|
||||
@@ -140,6 +148,7 @@ This is a **single-file, frontend-only change** to `.pi/extensions/prod-logs/ind
|
||||
| `fetchLogs` / Coolify API | **No change** — data fetching unchanged |
|
||||
|
||||
### Deprecation/Migration
|
||||
|
||||
None. Old behavior (scroll + refresh + close) is preserved and extended, not replaced.
|
||||
|
||||
---
|
||||
@@ -175,17 +184,19 @@ 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 |
|
||||
| `.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
|
||||
@@ -107,7 +110,7 @@ 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** |
|
||||
@@ -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.
|
||||
|
||||
@@ -237,7 +249,7 @@ 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 |
|
||||
| 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 |
|
||||
@@ -255,6 +267,7 @@ In-browser fetches (filter toggles, load more, detail) use an inline loading ind
|
||||
**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 -->
|
||||
|
||||
+13
-4
@@ -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
|
||||
@@ -32,7 +33,7 @@ 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` |
|
||||
@@ -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 (30–60 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
|
||||
|
||||
+5
-2
@@ -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
|
||||
|
||||
+5
-2
@@ -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 -->
|
||||
|
||||
+5
-2
@@ -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
|
||||
|
||||
+5
-2
@@ -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 -->
|
||||
|
||||
+5
-2
@@ -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.
|
||||
|
||||
+5
-2
@@ -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
Reference in New Issue
Block a user