From dc3b31378d15479aff9bdb6fcc60073ae9406cc9 Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Mon, 4 May 2026 21:22:27 +0100 Subject: [PATCH] Run prettier on backlog folder --- ...ult-limit-to-Maintenance-orphan-queries.md | 14 +- ...nput-and-details-for-wishlisted-records.md | 6 +- ...-and-selection-bar-to-ScrobbleLive.Show.md | 5 +- ... - Replicate-CI-pipeline-on-tangled.org.md | 11 +- ...imeout-configuration-across-API-clients.md | 7 +- ...-artists-without-clear-canonical-source.md | 8 +- ...between-Records-and-Collection-Wishlist.md | 7 +- ...ON-extraction-queries-in-ListeningStats.md | 7 +- ...ndex-runs-4-sequential-queries-in-mount.md | 7 +- ...y-backoff-strategy-for-non-Last.fm-APIs.md | 7 +- ...ted-Oban-workers-for-refresh-operations.md | 7 +- ... Records-context-has-8-responsibilities.md | 8 +- ...c-only-coupling-from-Records-to-Artists.md | 10 +- ...arge-modules-unchanged-since-last-audit.md | 16 +- .../ml-68 - Random-data-in-test-fixtures.md | 6 +- ...rgumentError-raises-in-ScrobbleActivity.md | 6 +- ... Bare-catch-blocks-in-telemetry-storage.md | 6 +- ...dules-approaching-refactoring-threshold.md | 6 +- .../ml-89 - Random-data-in-test-fixtures.md | 6 +- ...→-Collection-→-Records-static-alias-cycle.md | 12 +- ...t-timestamp-type-in-ScrobbleRule-schema.md | 6 +- ...ml-101 - Artists-without-Wikipedia-page.md | 6 +- ...Universal-Search-to-search-across-notes.md | 6 +- ...est-purchase-artwork-in-stats-on-mobile.md | 6 +- ...ckup-database-to-Hetzner-object-storage.md | 6 +- ...5 - Refactor-MusicLibrary.ScrobbleRules.md | 5 +- .../ml-106 - Set-up-Copilot-instructions.md | 6 +- ...07 - Regular-job-to-cleanup-asset-cache.md | 6 +- ...niversal-search-navigation-via-keyboard.md | 6 +- .../completed/ml-109 - Improve-test-suite.md | 9 +- ...erver-funnels-synchronous-SQLite-writes.md | 9 +- backlog/completed/ml-110 - Radios.md | 6 +- ...iew-design-of-ad-hoc-scrobble-show-view.md | 6 +- ...12 - Optimise-scrobble-rule-application.md | 6 +- .../ml-113 - Scrobble-groups-of-tracks.md | 5 +- ... Display-progarchives-links-for-records.md | 6 +- .../ml-115 - Import-Last.fm-history.md | 6 +- ...play-similar-artists-with-an-image-grid.md | 6 +- ... - Move-artist-bio-to-a-left-side-sheet.md | 6 +- ...rtist-info-and-artwork-from-artist-page.md | 6 +- .../ml-119 - Scrobble-individual-media.md | 6 +- ...-of-Note.changeset-2-in-notes-component.md | 7 +- ...-listed-record-set-its-selected-release.md | 8 +- .../ml-121 - Disable-Last.fm-http-pool.md | 6 +- ...ze-of-record-form-save-button-on-mobile.md | 6 +- ...of-releases-in-the-edit-record-dropdown.md | 6 +- ...ay-total-runtime-for-the-entire-release.md | 6 +- ...-each-track-should-also-display-artists.md | 8 +- ...ch-one-with-title-and-duration-in-mm-ss.md | 8 +- ...-ordered-by-position-with-total-runtime.md | 8 +- ...tracklist-sheet-from-record-detail-page.md | 6 +- ...o_sqlite3-version-constraint-in-mix.exs.md | 9 +- ...rich-selected-release-input-and-display.md | 6 +- .../ml-131 - Connect-a-Last.fm-account.md | 6 +- .../ml-132 - Scrobble-a-record-to-Last.fm.md | 6 +- ...Show-a-record-track-list-with-durations.md | 6 +- ... Support-selecting-a-release-per-record.md | 6 +- .../ml-135 - Add-country-to-artist-page.md | 6 +- backlog/completed/ml-136 - Unicode-search.md | 6 +- ...yfill-missing-data-in-scrobble-activity.md | 5 +- ...-of-the-same-album-in-scrobble-activity.md | 6 +- ...missing-artist-IDs-in-scrobble-activity.md | 6 +- ...ys.get_state-in-error_notifier_test.exs.md | 9 +- .../completed/ml-140 - Get-artist-images.md | 5 +- .../ml-141 - Support-records-with-accents.md | 6 +- ...ve-scrobble-UI-in-the-Release-component.md | 11 +- ...multi-record-import-in-Add-Record-modal.md | 9 +- ...euse-Release-component-on-scrobble-page.md | 10 +- ...-limit-reset-headers-for-precise-snooze.md | 30 +- ...l-147 - Upgrade-to-ServerSentEvents-1.0.md | 8 +- ...selection-bar-text-visibility-in-Safari.md | 13 +- ...e-Wikipedia-bio_html-in-ArtistLive.Show.md | 8 +- ...ords-sub-contexts-to-reduce-module-size.md | 16 +- ...che-TTL-and-cache-invalidation-strategy.md | 10 +- ...-version-prefix-to-collection-endpoints.md | 15 +- ...ta-transformations-out-of-Record-schema.md | 12 +- ...on-in-runtime.exs-for-SQLite-extensions.md | 8 +- ...t-statistical-preamble-for-chat-context.md | 8 +- ...event-pi-from-accessing-sensitive-files.md | 127 ++- ...e-production-logs-to-single-line-format.md | 77 +- ...ect-and-copy-log-lines-from-log-browser.md | 58 +- ...xtension-for-interactive-error-browsing.md | 64 +- ...ntal-comment-left-in-Last.fm-API-client.md | 8 +- ...Verify-or-drop-multipart-hex-dependency.md | 7 +- ...rs-as-transient-vs-permanent-in-workers.md | 33 +- ...nt-function-pairs-duplicate-query-logic.md | 7 +- ...ts-is-755-lines-with-40-mixed-functions.md | 8 +- ...-issues-per-row-UPDATEs-instead-of-bulk.md | 7 +- ...ion-Wishlist-index-LiveView-duplication.md | 7 +- ...cordSetLive.RecordPicker-LiveComponents.md | 7 +- ...ession-in-Artists.refresh_lastfm_data-1.md | 7 +- ...ply-nested-subqueries-in-ListeningStats.md | 8 +- ...fe-bang-methods-and-lacks-rate-limiting.md | 7 +- ...fm-callback-errors-with-app-conventions.md | 7 +- ...n-resilient-to-color-extraction-failure.md | 7 +- ...asset-endpoint-against-invalid-payloads.md | 7 +- ...controlled-errors-from-genre-population.md | 7 +- ...-production-migration-strategy-mismatch.md | 7 +- ...t-Logger-style-in-workers-eager-vs-lazy.md | 8 +- ...dark-mode-class-in-search_components.ex.md | 6 +- ...lpers.RecordActions-and-Components.Chat.md | 7 +- ...pagination-query-styles-across-contexts.md | 8 +- ...tInfo-changeset-uniquely-casts-id-field.md | 8 +- ...key_type-on-Note-and-ArtistInfo-schemas.md | 6 +- ...- Wikipedia.Config-missing-enforce_keys.md | 6 +- ...-BraveSearch-APIs-missing-rate-limiting.md | 6 +- ..._-vs-refresh_-naming-in-Artists-context.md | 6 +- ...tistInfo-worker-contains-business-logic.md | 6 +- ...neAssets-worker-has-direct-Repo-queries.md | 6 +- ...Activity-context-has-zero-test-coverage.md | 5 +- ...codeScan-context-has-zero-test-coverage.md | 5 +- ...t-Last.fm-OAuth-callback-trust-boundary.md | 13 +- ...se-helpers-in-Collection-Wishlist-Index.md | 6 +- .../ml-51 - ArtistLive.Show-is-823-lines.md | 5 +- ...ion-Wishlist-Show-LiveViews-duplication.md | 6 +- ... - Rewrite-vertical-bar-chart-component.md | 6 +- .../ml-54 - Standardize-play-icon-sizing.md | 6 +- ...- Standardize-stats-dashboard-gap-sizes.md | 6 +- ...andardize-card-border-radius-and-shadow.md | 6 +- ...ize-grid-gap-values-across-record-grids.md | 6 +- ...58 - Standardize-page-header-top-margin.md | 6 +- ...-separation-pattern-divide-y-vs-space-y.md | 6 +- ...threshold-with-sandbox-achievable-level.md | 8 +- ...re-tag-styling-between-edit-and-display.md | 6 +- ...ze-dropdown-toggle-variant-across-pages.md | 6 +- ...-artist-queries-through-Artists-context.md | 5 +- ...-dependency-between-Records-and-Artists.md | 5 +- ...t-ScrobbleActivity-into-focused-modules.md | 5 +- ...scrobble-queries-out-of-Records-context.md | 5 +- ... - Credo-ModuleDoc-check-still-disabled.md | 6 +- ...s-in-scrobble_activity-and-worker-tests.md | 7 +- ... - page_title-2-public-in-two-LiveViews.md | 6 +- ...lbums-TopArtists-structural-duplication.md | 6 +- ...put-in-search-and-record-set-components.md | 6 +- ...markdown-HTML-output-in-Notes-component.md | 6 +- ...urity-Policy-header-to-browser-pipeline.md | 6 +- ...onsistent-alphabetical-ordering-helpers.md | 6 +- ...8 - Commented-out-code-in-error_json.ex.md | 6 +- ...lementations-have-trivial-test-coverage.md | 6 +- ...o-Dependabot-for-Dockerfile-base-images.md | 7 +- ...0 - ScrobbleLive-has-zero-test-coverage.md | 6 +- ...nsistent-toast-arity-in-form-components.md | 8 +- ...nceLive.Index-queries-database-directly.md | 6 +- ...apply_fallback_index-across-5-LiveViews.md | 6 +- ...-FK-index-on-record_set_items.record_id.md | 6 +- ...ted-String.to_integer-in-HTTP-endpoints.md | 6 +- .../ml-86 - Scrobble-data-enrichment.md | 6 +- ...stInfo.country-1-assumes-map-key-exists.md | 6 +- ...dcoded-genre-filter-in-collection-stats.md | 6 +- ...d-mix_audit-dependency-for-CVE-scanning.md | 12 +- ...l-90 - Credo-complexity-checks-disabled.md | 6 +- ...obbleActivity-is-a-god-module-557-lines.md | 6 +- ...tion-in-ScrobbleRules-context-819-lines.md | 6 +- ...ason-exposed-to-users-in-error-messages.md | 6 +- ...tore_image-pattern-match-in-artist-form.md | 6 +- ...sleep-for-rate-limiting-in-Oban-workers.md | 16 +- ...ing-schema-validations-on-string-fields.md | 6 +- ...e-definitions-in-tests-partially-solved.md | 6 +- ...Potential-nil-crash-in-ScrobbleActivity.md | 6 +- ...h-string-concatenation-in-ScrobbleRules.md | 6 +- ...or-reducing-collection-chat-token-usage.md | 42 +- ...event-pi-from-accessing-sensitive-files.md | 72 +- ...e-production-logs-to-single-line-format.md | 35 +- ...c-4 - GitHub-Actions-→-Tangled-Spindles.md | 53 +- ... - Log-Line-Copy-in-Prod-Logs-Extension.md | 22 +- ... Programmatic-Access-to-Production-Logs.md | 13 +- .../doc-7 - Pi-access-to-production-errors.md | 15 +- backlog/ml-142/mockups.html | 551 ++++++++-- backlog/ml-142/plan.md | 16 +- backlog/ml-143/mockups.html | 968 ++++++++++++++---- backlog/ml-143/plan.md | 18 + ...ement-magazine-issue-ingestion-pipeline.md | 14 +- ...tion-context-to-LLM-for-collection-chat.md | 26 +- ... Programmatic-access-to-production-logs.md | 54 +- ...ml-161 - pi-access-to-production-errors.md | 10 +- ...production-errors-via-JSON-API-endpoint.md | 57 +- ...ate-pi-tools-to-fetch-production-errors.md | 89 +- 177 files changed, 2594 insertions(+), 936 deletions(-) diff --git a/backlog/archive/tasks/ml-1 - Add-default-limit-to-Maintenance-orphan-queries.md b/backlog/archive/tasks/ml-1 - Add-default-limit-to-Maintenance-orphan-queries.md index 11fd95d1..d828401e 100644 --- a/backlog/archive/tasks/ml-1 - Add-default-limit-to-Maintenance-orphan-queries.md +++ b/backlog/archive/tasks/ml-1 - Add-default-limit-to-Maintenance-orphan-queries.md @@ -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 + _GitHub: created 2026-04-16 · updated 2026-04-16_ ## Summary @@ -38,10 +39,13 @@ def get_artists_missing_musicbrainz_id(opts \\ []) do # ... end ``` + ## Acceptance Criteria + + - [ ] #1 Both functions apply a default limit when none is supplied - [ ] #2 Existing callers continue to override when they need a different value @@ -49,6 +53,7 @@ end ## Final Summary + 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. + diff --git a/backlog/archive/tasks/ml-123 - Hide-collected-release-input-and-details-for-wishlisted-records.md b/backlog/archive/tasks/ml-123 - Hide-collected-release-input-and-details-for-wishlisted-records.md index d3ff8ddc..7291feca 100644 --- a/backlog/archive/tasks/ml-123 - Hide-collected-release-input-and-details-for-wishlisted-records.md +++ b/backlog/archive/tasks/ml-123 - Hide-collected-release-input-and-details-for-wishlisted-records.md @@ -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 + _GitHub: created 2025-05-07 · updated 2025-05-08 · closed 2025-05-08 · not planned_ + diff --git a/backlog/archive/tasks/ml-142.1 - Port-Finished-at-picker-and-selection-bar-to-ScrobbleLive.Show.md b/backlog/archive/tasks/ml-142.1 - Port-Finished-at-picker-and-selection-bar-to-ScrobbleLive.Show.md index 777062b4..a7665069 100644 --- a/backlog/archive/tasks/ml-142.1 - Port-Finished-at-picker-and-selection-bar-to-ScrobbleLive.Show.md +++ b/backlog/archive/tasks/ml-142.1 - Port-Finished-at-picker-and-selection-bar-to-ScrobbleLive.Show.md @@ -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 + 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 ## Acceptance Criteria + + - [ ] #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 diff --git a/backlog/archive/tasks/ml-159 - Replicate-CI-pipeline-on-tangled.org.md b/backlog/archive/tasks/ml-159 - Replicate-CI-pipeline-on-tangled.org.md index 9227650d..36e44dcf 100644 --- a/backlog/archive/tasks/ml-159 - Replicate-CI-pipeline-on-tangled.org.md +++ b/backlog/archive/tasks/ml-159 - Replicate-CI-pipeline-on-tangled.org.md @@ -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 + 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. + ## Final Summary + 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. + diff --git a/backlog/archive/tasks/ml-16 - Inconsistent-HTTP-timeout-configuration-across-API-clients.md b/backlog/archive/tasks/ml-16 - Inconsistent-HTTP-timeout-configuration-across-API-clients.md index 964f3fb0..7d334172 100644 --- a/backlog/archive/tasks/ml-16 - Inconsistent-HTTP-timeout-configuration-across-API-clients.md +++ b/backlog/archive/tasks/ml-16 - Inconsistent-HTTP-timeout-configuration-across-API-clients.md @@ -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 + _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 + + - All API clients have explicit, intentional timeout configuration - Timeout choices are documented (e.g., in Config module comments) diff --git a/backlog/archive/tasks/ml-18 - Three-representations-of-artists-without-clear-canonical-source.md b/backlog/archive/tasks/ml-18 - Three-representations-of-artists-without-clear-canonical-source.md index e5404b5d..824f9e6a 100644 --- a/backlog/archive/tasks/ml-18 - Three-representations-of-artists-without-clear-canonical-source.md +++ b/backlog/archive/tasks/ml-18 - Three-representations-of-artists-without-clear-canonical-source.md @@ -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 + _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 + + - Clear documentation of each representation's purpose - Reduced confusion about which to use in new features diff --git a/backlog/archive/tasks/ml-19 - Circular-dependency-between-Records-and-Collection-Wishlist.md b/backlog/archive/tasks/ml-19 - Circular-dependency-between-Records-and-Collection-Wishlist.md index eb839312..4cde847b 100644 --- a/backlog/archive/tasks/ml-19 - Circular-dependency-between-Records-and-Collection-Wishlist.md +++ b/backlog/archive/tasks/ml-19 - Circular-dependency-between-Records-and-Collection-Wishlist.md @@ -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 + _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 + + - No circular dependency between context modules - Collection/Wishlist filtering remains functional diff --git a/backlog/archive/tasks/ml-20 - Missing-indexes-on-JSON-extraction-queries-in-ListeningStats.md b/backlog/archive/tasks/ml-20 - Missing-indexes-on-JSON-extraction-queries-in-ListeningStats.md index c4921640..ab00c5ce 100644 --- a/backlog/archive/tasks/ml-20 - Missing-indexes-on-JSON-extraction-queries-in-ListeningStats.md +++ b/backlog/archive/tasks/ml-20 - Missing-indexes-on-JSON-extraction-queries-in-ListeningStats.md @@ -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 + _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 + + - Expression-based indexes exist for hot JSON extraction paths - Query plans show index usage for filtered ListeningStats queries diff --git a/backlog/archive/tasks/ml-23 - StatsLive.Index-runs-4-sequential-queries-in-mount.md b/backlog/archive/tasks/ml-23 - StatsLive.Index-runs-4-sequential-queries-in-mount.md index 913dd30e..f0f663eb 100644 --- a/backlog/archive/tasks/ml-23 - StatsLive.Index-runs-4-sequential-queries-in-mount.md +++ b/backlog/archive/tasks/ml-23 - StatsLive.Index-runs-4-sequential-queries-in-mount.md @@ -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 + _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 + + - Stats page initial render is faster - Data loads concurrently where possible - Loading states are shown while data is fetched diff --git a/backlog/archive/tasks/ml-26 - No-retry-backoff-strategy-for-non-Last.fm-APIs.md b/backlog/archive/tasks/ml-26 - No-retry-backoff-strategy-for-non-Last.fm-APIs.md index e455ffd4..1a8a21b7 100644 --- a/backlog/archive/tasks/ml-26 - No-retry-backoff-strategy-for-non-Last.fm-APIs.md +++ b/backlog/archive/tasks/ml-26 - No-retry-backoff-strategy-for-non-Last.fm-APIs.md @@ -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 + _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 + + - API-specific workers handle rate-limit responses with appropriate snooze durations - Transient errors use exponential backoff rather than immediate retry diff --git a/backlog/archive/tasks/ml-27 - 11-copy-pasted-Oban-workers-for-refresh-operations.md b/backlog/archive/tasks/ml-27 - 11-copy-pasted-Oban-workers-for-refresh-operations.md index 73692ae5..5a4b111b 100644 --- a/backlog/archive/tasks/ml-27 - 11-copy-pasted-Oban-workers-for-refresh-operations.md +++ b/backlog/archive/tasks/ml-27 - 11-copy-pasted-Oban-workers-for-refresh-operations.md @@ -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 + _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 + + - Boilerplate is reduced - Each worker remains individually identifiable for Oban queue routing and monitoring - No change in worker behaviour diff --git a/backlog/archive/tasks/ml-28 - Records-context-has-8-responsibilities.md b/backlog/archive/tasks/ml-28 - Records-context-has-8-responsibilities.md index 763d1c0d..10ae22b4 100644 --- a/backlog/archive/tasks/ml-28 - Records-context-has-8-responsibilities.md +++ b/backlog/archive/tasks/ml-28 - Records-context-has-8-responsibilities.md @@ -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 + _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 + + - Each sub-module has a single clear responsibility - Public API remains unchanged for callers - No regression in functionality diff --git a/backlog/archive/tasks/ml-3 - Document-intentional-async-only-coupling-from-Records-to-Artists.md b/backlog/archive/tasks/ml-3 - Document-intentional-async-only-coupling-from-Records-to-Artists.md index 47720f64..0e09e191 100644 --- a/backlog/archive/tasks/ml-3 - Document-intentional-async-only-coupling-from-Records-to-Artists.md +++ b/backlog/archive/tasks/ml-3 - Document-intentional-async-only-coupling-from-Records-to-Artists.md @@ -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 + _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) ``` + ## Acceptance Criteria + + - [ ] #1 Inline comments added at records.ex:385 and :415 - [ ] #2 Comments explain the runtime-cycle motivation, not just "it's async" diff --git a/backlog/archive/tasks/ml-66 - Large-modules-unchanged-since-last-audit.md b/backlog/archive/tasks/ml-66 - Large-modules-unchanged-since-last-audit.md index aeb984eb..22c200de 100644 --- a/backlog/archive/tasks/ml-66 - Large-modules-unchanged-since-last-audit.md +++ b/backlog/archive/tasks/ml-66 - Large-modules-unchanged-since-last-audit.md @@ -3,29 +3,30 @@ id: ML-66 title: Large modules unchanged since last audit status: To Do assignee: [] -created_date: '2026-04-20 08:55' +created_date: "2026-04-20 08:55" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/109' + - "https://github.com/cloud8421/music_library/issues/109" priority: low --- ## Description + _GitHub: created 2026-03-12 · updated 2026-04-09 · closed 2026-03-16 · not planned_ ## Description The four modules flagged in #99 remain at the same size: -| File | LOC | -|------|-----| -| `lib/music_library_web/live/artist_live/show.ex` | 804 | +| File | LOC | +| ------------------------------------------------------- | --- | +| `lib/music_library_web/live/artist_live/show.ex` | 804 | | `lib/music_library_web/components/record_components.ex` | 761 | -| `lib/music_library_web/components/record_form.ex` | 652 | -| `lib/music_brainz/api.ex` | 564 | +| `lib/music_library_web/components/record_form.ex` | 652 | +| `lib/music_brainz/api.ex` | 564 | Not urgent — only refactor when new feature work naturally touches these files. @@ -36,4 +37,5 @@ Monitor and consider extraction when adding new features to these modules. ## Source From technical debt audit (2026-03-12). Continuation of #99. + diff --git a/backlog/archive/tasks/ml-68 - Random-data-in-test-fixtures.md b/backlog/archive/tasks/ml-68 - Random-data-in-test-fixtures.md index 7e30fbb4..23c603a2 100644 --- a/backlog/archive/tasks/ml-68 - Random-data-in-test-fixtures.md +++ b/backlog/archive/tasks/ml-68 - Random-data-in-test-fixtures.md @@ -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 + _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. + diff --git a/backlog/archive/tasks/ml-69 - ArgumentError-raises-in-ScrobbleActivity.md b/backlog/archive/tasks/ml-69 - ArgumentError-raises-in-ScrobbleActivity.md index 318c66db..2ecd959e 100644 --- a/backlog/archive/tasks/ml-69 - ArgumentError-raises-in-ScrobbleActivity.md +++ b/backlog/archive/tasks/ml-69 - ArgumentError-raises-in-ScrobbleActivity.md @@ -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 + _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). + diff --git a/backlog/archive/tasks/ml-70 - Bare-catch-blocks-in-telemetry-storage.md b/backlog/archive/tasks/ml-70 - Bare-catch-blocks-in-telemetry-storage.md index af162d54..9a10be4c 100644 --- a/backlog/archive/tasks/ml-70 - Bare-catch-blocks-in-telemetry-storage.md +++ b/backlog/archive/tasks/ml-70 - Bare-catch-blocks-in-telemetry-storage.md @@ -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 + _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). + diff --git a/backlog/archive/tasks/ml-76 - Large-modules-approaching-refactoring-threshold.md b/backlog/archive/tasks/ml-76 - Large-modules-approaching-refactoring-threshold.md index a025c59b..6b497641 100644 --- a/backlog/archive/tasks/ml-76 - Large-modules-approaching-refactoring-threshold.md +++ b/backlog/archive/tasks/ml-76 - Large-modules-approaching-refactoring-threshold.md @@ -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 + _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. + diff --git a/backlog/archive/tasks/ml-89 - Random-data-in-test-fixtures.md b/backlog/archive/tasks/ml-89 - Random-data-in-test-fixtures.md index 1cace336..4c59b27f 100644 --- a/backlog/archive/tasks/ml-89 - Random-data-in-test-fixtures.md +++ b/backlog/archive/tasks/ml-89 - Random-data-in-test-fixtures.md @@ -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 + _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. + diff --git a/backlog/completed/ml-10 - Break-Records-→-Artists-→-Collection-→-Records-static-alias-cycle.md b/backlog/completed/ml-10 - Break-Records-→-Artists-→-Collection-→-Records-static-alias-cycle.md index 38889447..309b4362 100644 --- a/backlog/completed/ml-10 - Break-Records-→-Artists-→-Collection-→-Records-static-alias-cycle.md +++ b/backlog/completed/ml-10 - Break-Records-→-Artists-→-Collection-→-Records-static-alias-cycle.md @@ -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 + _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. + ## Acceptance Criteria + + - `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 + 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. + diff --git a/backlog/completed/ml-100 - Inconsistent-timestamp-type-in-ScrobbleRule-schema.md b/backlog/completed/ml-100 - Inconsistent-timestamp-type-in-ScrobbleRule-schema.md index 59c4fabc..aecde561 100644 --- a/backlog/completed/ml-100 - Inconsistent-timestamp-type-in-ScrobbleRule-schema.md +++ b/backlog/completed/ml-100 - Inconsistent-timestamp-type-in-ScrobbleRule-schema.md @@ -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 + _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. + diff --git a/backlog/completed/ml-101 - Artists-without-Wikipedia-page.md b/backlog/completed/ml-101 - Artists-without-Wikipedia-page.md index 330d71bb..ac1f243a 100644 --- a/backlog/completed/ml-101 - Artists-without-Wikipedia-page.md +++ b/backlog/completed/ml-101 - Artists-without-Wikipedia-page.md @@ -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 + _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 ``` + diff --git a/backlog/completed/ml-102 - Extend-Universal-Search-to-search-across-notes.md b/backlog/completed/ml-102 - Extend-Universal-Search-to-search-across-notes.md index da8c4fe4..6ede07b6 100644 --- a/backlog/completed/ml-102 - Extend-Universal-Search-to-search-across-notes.md +++ b/backlog/completed/ml-102 - Extend-Universal-Search-to-search-across-notes.md @@ -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 + _GitHub: created 2026-01-08 · updated 2026-02-10 · closed 2026-02-10_ Results should include a small preview of the result. + diff --git a/backlog/completed/ml-103 - Increase-size-of-latest-purchase-artwork-in-stats-on-mobile.md b/backlog/completed/ml-103 - Increase-size-of-latest-purchase-artwork-in-stats-on-mobile.md index b06d76fb..8933bec5 100644 --- a/backlog/completed/ml-103 - Increase-size-of-latest-purchase-artwork-in-stats-on-mobile.md +++ b/backlog/completed/ml-103 - Increase-size-of-latest-purchase-artwork-in-stats-on-mobile.md @@ -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 + _GitHub: created 2025-12-24 · updated 2025-12-24 · closed 2025-12-24_ Make it slightly bigger, making sure image link uses appropriate dimensions. + diff --git a/backlog/completed/ml-104 - Setup-Litestream-to-backup-database-to-Hetzner-object-storage.md b/backlog/completed/ml-104 - Setup-Litestream-to-backup-database-to-Hetzner-object-storage.md index a113ff70..e0afec44 100644 --- a/backlog/completed/ml-104 - Setup-Litestream-to-backup-database-to-Hetzner-object-storage.md +++ b/backlog/completed/ml-104 - Setup-Litestream-to-backup-database-to-Hetzner-object-storage.md @@ -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 + _GitHub: created 2025-12-12 · updated 2026-03-20 · closed 2026-03-20_ + diff --git a/backlog/completed/ml-105 - Refactor-MusicLibrary.ScrobbleRules.md b/backlog/completed/ml-105 - Refactor-MusicLibrary.ScrobbleRules.md index bf003bb1..6cd2318c 100644 --- a/backlog/completed/ml-105 - Refactor-MusicLibrary.ScrobbleRules.md +++ b/backlog/completed/ml-105 - Refactor-MusicLibrary.ScrobbleRules.md @@ -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 + _GitHub: created 2025-11-16 · updated 2026-02-10 · closed 2026-02-10_ - Extract sql macro for `CASE...WHEN...ELSE` update diff --git a/backlog/completed/ml-106 - Set-up-Copilot-instructions.md b/backlog/completed/ml-106 - Set-up-Copilot-instructions.md index f99ba60d..bc928653 100644 --- a/backlog/completed/ml-106 - Set-up-Copilot-instructions.md +++ b/backlog/completed/ml-106 - Set-up-Copilot-instructions.md @@ -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 + _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. + diff --git a/backlog/completed/ml-107 - Regular-job-to-cleanup-asset-cache.md b/backlog/completed/ml-107 - Regular-job-to-cleanup-asset-cache.md index 7d6dd1ff..b2dfbe4c 100644 --- a/backlog/completed/ml-107 - Regular-job-to-cleanup-asset-cache.md +++ b/backlog/completed/ml-107 - Regular-job-to-cleanup-asset-cache.md @@ -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 + _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; ``` + diff --git a/backlog/completed/ml-108 - Support-universal-search-navigation-via-keyboard.md b/backlog/completed/ml-108 - Support-universal-search-navigation-via-keyboard.md index 5f1e13a0..9c7541b0 100644 --- a/backlog/completed/ml-108 - Support-universal-search-navigation-via-keyboard.md +++ b/backlog/completed/ml-108 - Support-universal-search-navigation-via-keyboard.md @@ -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 + _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. + diff --git a/backlog/completed/ml-109 - Improve-test-suite.md b/backlog/completed/ml-109 - Improve-test-suite.md index 2f6318e8..c60a77bc 100644 --- a/backlog/completed/ml-109 - Improve-test-suite.md +++ b/backlog/completed/ml-109 - Improve-test-suite.md @@ -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 + _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. + diff --git a/backlog/completed/ml-11 - Telemetry.Storage-GenServer-funnels-synchronous-SQLite-writes.md b/backlog/completed/ml-11 - Telemetry.Storage-GenServer-funnels-synchronous-SQLite-writes.md index fb0c2fc3..aa2d213e 100644 --- a/backlog/completed/ml-11 - Telemetry.Storage-GenServer-funnels-synchronous-SQLite-writes.md +++ b/backlog/completed/ml-11 - Telemetry.Storage-GenServer-funnels-synchronous-SQLite-writes.md @@ -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 + _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 + + - 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 diff --git a/backlog/completed/ml-110 - Radios.md b/backlog/completed/ml-110 - Radios.md index 75ce79a8..97d94058 100644 --- a/backlog/completed/ml-110 - Radios.md +++ b/backlog/completed/ml-110 - Radios.md @@ -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 + _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. + diff --git a/backlog/completed/ml-111 - Review-design-of-ad-hoc-scrobble-show-view.md b/backlog/completed/ml-111 - Review-design-of-ad-hoc-scrobble-show-view.md index 970cc6c9..4a886c89 100644 --- a/backlog/completed/ml-111 - Review-design-of-ad-hoc-scrobble-show-view.md +++ b/backlog/completed/ml-111 - Review-design-of-ad-hoc-scrobble-show-view.md @@ -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 + _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. + diff --git a/backlog/completed/ml-112 - Optimise-scrobble-rule-application.md b/backlog/completed/ml-112 - Optimise-scrobble-rule-application.md index 6c889dc1..1e655eed 100644 --- a/backlog/completed/ml-112 - Optimise-scrobble-rule-application.md +++ b/backlog/completed/ml-112 - Optimise-scrobble-rule-application.md @@ -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 + _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. + diff --git a/backlog/completed/ml-113 - Scrobble-groups-of-tracks.md b/backlog/completed/ml-113 - Scrobble-groups-of-tracks.md index 64be3c6d..aede3eec 100644 --- a/backlog/completed/ml-113 - Scrobble-groups-of-tracks.md +++ b/backlog/completed/ml-113 - Scrobble-groups-of-tracks.md @@ -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 + _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. diff --git a/backlog/completed/ml-114 - Display-progarchives-links-for-records.md b/backlog/completed/ml-114 - Display-progarchives-links-for-records.md index a391a7fb..41613b74 100644 --- a/backlog/completed/ml-114 - Display-progarchives-links-for-records.md +++ b/backlog/completed/ml-114 - Display-progarchives-links-for-records.md @@ -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 + _GitHub: created 2025-05-25 · updated 2026-02-10 · closed 2026-02-10_ Where applicable. + diff --git a/backlog/completed/ml-115 - Import-Last.fm-history.md b/backlog/completed/ml-115 - Import-Last.fm-history.md index 80dee726..fa2116da 100644 --- a/backlog/completed/ml-115 - Import-Last.fm-history.md +++ b/backlog/completed/ml-115 - Import-Last.fm-history.md @@ -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 + _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. + diff --git a/backlog/completed/ml-116 - Display-similar-artists-with-an-image-grid.md b/backlog/completed/ml-116 - Display-similar-artists-with-an-image-grid.md index a3f7b347..7a4c2038 100644 --- a/backlog/completed/ml-116 - Display-similar-artists-with-an-image-grid.md +++ b/backlog/completed/ml-116 - Display-similar-artists-with-an-image-grid.md @@ -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 + _GitHub: created 2025-05-21 · updated 2025-05-21 · closed 2025-05-21_ + diff --git a/backlog/completed/ml-117 - Move-artist-bio-to-a-left-side-sheet.md b/backlog/completed/ml-117 - Move-artist-bio-to-a-left-side-sheet.md index 009d6ec3..19e95fad 100644 --- a/backlog/completed/ml-117 - Move-artist-bio-to-a-left-side-sheet.md +++ b/backlog/completed/ml-117 - Move-artist-bio-to-a-left-side-sheet.md @@ -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 + _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. + diff --git a/backlog/completed/ml-118 - Add-actions-to-refresh-artist-info-and-artwork-from-artist-page.md b/backlog/completed/ml-118 - Add-actions-to-refresh-artist-info-and-artwork-from-artist-page.md index de05cf8d..ccbc9b9e 100644 --- a/backlog/completed/ml-118 - Add-actions-to-refresh-artist-info-and-artwork-from-artist-page.md +++ b/backlog/completed/ml-118 - Add-actions-to-refresh-artist-info-and-artwork-from-artist-page.md @@ -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 + _GitHub: created 2025-05-20 · updated 2025-05-20 · closed 2025-05-20_ + diff --git a/backlog/completed/ml-119 - Scrobble-individual-media.md b/backlog/completed/ml-119 - Scrobble-individual-media.md index a8945fb6..7325a145 100644 --- a/backlog/completed/ml-119 - Scrobble-individual-media.md +++ b/backlog/completed/ml-119 - Scrobble-individual-media.md @@ -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 + _GitHub: created 2025-05-09 · updated 2025-05-10 · closed 2025-05-10_ So that it's possible to scrobble individual discs. + diff --git a/backlog/completed/ml-12 - Use-Notes.change_note-2-instead-of-Note.changeset-2-in-notes-component.md b/backlog/completed/ml-12 - Use-Notes.change_note-2-instead-of-Note.changeset-2-in-notes-component.md index c7af948e..9cb753f3 100644 --- a/backlog/completed/ml-12 - Use-Notes.change_note-2-instead-of-Note.changeset-2-in-notes-component.md +++ b/backlog/completed/ml-12 - Use-Notes.change_note-2-instead-of-Note.changeset-2-in-notes-component.md @@ -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 + _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 + + - Tests still pass - No direct `Note.changeset` references in `lib/music_library_web/components/notes.ex` diff --git a/backlog/completed/ml-120 - When-scanning-a-wish-listed-record-set-its-selected-release.md b/backlog/completed/ml-120 - When-scanning-a-wish-listed-record-set-its-selected-release.md index ecbecc00..43125311 100644 --- a/backlog/completed/ml-120 - When-scanning-a-wish-listed-record-set-its-selected-release.md +++ b/backlog/completed/ml-120 - When-scanning-a-wish-listed-record-set-its-selected-release.md @@ -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 + _GitHub: created 2025-05-09 · updated 2025-05-09 · closed 2025-05-09_ + diff --git a/backlog/completed/ml-121 - Disable-Last.fm-http-pool.md b/backlog/completed/ml-121 - Disable-Last.fm-http-pool.md index b67122ef..eb946345 100644 --- a/backlog/completed/ml-121 - Disable-Last.fm-http-pool.md +++ b/backlog/completed/ml-121 - Disable-Last.fm-http-pool.md @@ -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 + _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. + diff --git a/backlog/completed/ml-122 - Increase-size-of-record-form-save-button-on-mobile.md b/backlog/completed/ml-122 - Increase-size-of-record-form-save-button-on-mobile.md index 02350685..94f603a3 100644 --- a/backlog/completed/ml-122 - Increase-size-of-record-form-save-button-on-mobile.md +++ b/backlog/completed/ml-122 - Increase-size-of-record-form-save-button-on-mobile.md @@ -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 + _GitHub: created 2025-05-09 · updated 2025-05-09 · closed 2025-05-09_ + diff --git a/backlog/completed/ml-124 - Improve-display-of-releases-in-the-edit-record-dropdown.md b/backlog/completed/ml-124 - Improve-display-of-releases-in-the-edit-record-dropdown.md index 51affa24..04e460ef 100644 --- a/backlog/completed/ml-124 - Improve-display-of-releases-in-the-edit-record-dropdown.md +++ b/backlog/completed/ml-124 - Improve-display-of-releases-in-the-edit-record-dropdown.md @@ -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 + _GitHub: created 2025-05-07 · updated 2025-05-07 · closed 2025-05-07_ Sort by date descending, country + diff --git a/backlog/completed/ml-125 - Display-total-runtime-for-the-entire-release.md b/backlog/completed/ml-125 - Display-total-runtime-for-the-entire-release.md index 74fec91d..92c3f11f 100644 --- a/backlog/completed/ml-125 - Display-total-runtime-for-the-entire-release.md +++ b/backlog/completed/ml-125 - Display-total-runtime-for-the-entire-release.md @@ -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 + _GitHub: created 2025-05-04 · updated 2025-05-05 · closed 2025-05-05_ + diff --git a/backlog/completed/ml-126 - For-compilations-each-track-should-also-display-artists.md b/backlog/completed/ml-126 - For-compilations-each-track-should-also-display-artists.md index 3ccb16f1..3eaac41a 100644 --- a/backlog/completed/ml-126 - For-compilations-each-track-should-also-display-artists.md +++ b/backlog/completed/ml-126 - For-compilations-each-track-should-also-display-artists.md @@ -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 + _GitHub: created 2025-05-04 · updated 2025-05-04 · closed 2025-05-04_ + diff --git a/backlog/completed/ml-127 - Display-tracks-each-one-with-title-and-duration-in-mm-ss.md b/backlog/completed/ml-127 - Display-tracks-each-one-with-title-and-duration-in-mm-ss.md index 46fb381a..b9ee9c24 100644 --- a/backlog/completed/ml-127 - Display-tracks-each-one-with-title-and-duration-in-mm-ss.md +++ b/backlog/completed/ml-127 - Display-tracks-each-one-with-title-and-duration-in-mm-ss.md @@ -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 + _GitHub: created 2025-05-04 · updated 2025-05-05 · closed 2025-05-05_ + diff --git a/backlog/completed/ml-128 - Display-media-discs-ordered-by-position-with-total-runtime.md b/backlog/completed/ml-128 - Display-media-discs-ordered-by-position-with-total-runtime.md index a85ffb1d..5e55b1c0 100644 --- a/backlog/completed/ml-128 - Display-media-discs-ordered-by-position-with-total-runtime.md +++ b/backlog/completed/ml-128 - Display-media-discs-ordered-by-position-with-total-runtime.md @@ -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 + _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 + diff --git a/backlog/completed/ml-129 - Display-tracklist-sheet-from-record-detail-page.md b/backlog/completed/ml-129 - Display-tracklist-sheet-from-record-detail-page.md index 0029c851..26e14afd 100644 --- a/backlog/completed/ml-129 - Display-tracklist-sheet-from-record-detail-page.md +++ b/backlog/completed/ml-129 - Display-tracklist-sheet-from-record-detail-page.md @@ -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 + _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. + diff --git a/backlog/completed/ml-13 - Tighten-ecto_sqlite3-version-constraint-in-mix.exs.md b/backlog/completed/ml-13 - Tighten-ecto_sqlite3-version-constraint-in-mix.exs.md index 72aa7de4..e1bc31f3 100644 --- a/backlog/completed/ml-13 - Tighten-ecto_sqlite3-version-constraint-in-mix.exs.md +++ b/backlog/completed/ml-13 - Tighten-ecto_sqlite3-version-constraint-in-mix.exs.md @@ -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 + _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 + + - `mix deps.get` still resolves cleanly - `mix test` passes diff --git a/backlog/completed/ml-130 - Enrich-selected-release-input-and-display.md b/backlog/completed/ml-130 - Enrich-selected-release-input-and-display.md index f7454678..3f68826f 100644 --- a/backlog/completed/ml-130 - Enrich-selected-release-input-and-display.md +++ b/backlog/completed/ml-130 - Enrich-selected-release-input-and-display.md @@ -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 + _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. + diff --git a/backlog/completed/ml-131 - Connect-a-Last.fm-account.md b/backlog/completed/ml-131 - Connect-a-Last.fm-account.md index a478ea25..ab74f641 100644 --- a/backlog/completed/ml-131 - Connect-a-Last.fm-account.md +++ b/backlog/completed/ml-131 - Connect-a-Last.fm-account.md @@ -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 + _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. + diff --git a/backlog/completed/ml-132 - Scrobble-a-record-to-Last.fm.md b/backlog/completed/ml-132 - Scrobble-a-record-to-Last.fm.md index 23f3ff78..46285639 100644 --- a/backlog/completed/ml-132 - Scrobble-a-record-to-Last.fm.md +++ b/backlog/completed/ml-132 - Scrobble-a-record-to-Last.fm.md @@ -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 + _GitHub: created 2025-04-30 · updated 2025-05-07 · closed 2025-05-07_ + diff --git a/backlog/completed/ml-133 - Show-a-record-track-list-with-durations.md b/backlog/completed/ml-133 - Show-a-record-track-list-with-durations.md index 43847a1e..759fc938 100644 --- a/backlog/completed/ml-133 - Show-a-record-track-list-with-durations.md +++ b/backlog/completed/ml-133 - Show-a-record-track-list-with-durations.md @@ -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 + _GitHub: created 2025-04-30 · updated 2025-05-05 · closed 2025-05-05_ Only for collected records with a selected release. + diff --git a/backlog/completed/ml-134 - Support-selecting-a-release-per-record.md b/backlog/completed/ml-134 - Support-selecting-a-release-per-record.md index 321b0fb7..eb8dfe5c 100644 --- a/backlog/completed/ml-134 - Support-selecting-a-release-per-record.md +++ b/backlog/completed/ml-134 - Support-selecting-a-release-per-record.md @@ -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 + _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. + diff --git a/backlog/completed/ml-135 - Add-country-to-artist-page.md b/backlog/completed/ml-135 - Add-country-to-artist-page.md index 902ed5f5..bc0fb792 100644 --- a/backlog/completed/ml-135 - Add-country-to-artist-page.md +++ b/backlog/completed/ml-135 - Add-country-to-artist-page.md @@ -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 + _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. + diff --git a/backlog/completed/ml-136 - Unicode-search.md b/backlog/completed/ml-136 - Unicode-search.md index f7068f2e..df141c27 100644 --- a/backlog/completed/ml-136 - Unicode-search.md +++ b/backlog/completed/ml-136 - Unicode-search.md @@ -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 + _GitHub: created 2025-04-26 · updated 2025-05-25 · closed 2025-05-25_ Searching for "bjorn" should return records, but it doesn't. + diff --git a/backlog/completed/ml-137 - Polyfill-missing-data-in-scrobble-activity.md b/backlog/completed/ml-137 - Polyfill-missing-data-in-scrobble-activity.md index 434a1c34..10096a70 100644 --- a/backlog/completed/ml-137 - Polyfill-missing-data-in-scrobble-activity.md +++ b/backlog/completed/ml-137 - Polyfill-missing-data-in-scrobble-activity.md @@ -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 + _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. diff --git a/backlog/completed/ml-138 - Allow-multiple-plays-of-the-same-album-in-scrobble-activity.md b/backlog/completed/ml-138 - Allow-multiple-plays-of-the-same-album-in-scrobble-activity.md index 05d34635..c244c3ef 100644 --- a/backlog/completed/ml-138 - Allow-multiple-plays-of-the-same-album-in-scrobble-activity.md +++ b/backlog/completed/ml-138 - Allow-multiple-plays-of-the-same-album-in-scrobble-activity.md @@ -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 + _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. + diff --git a/backlog/completed/ml-139 - Polyfill-missing-artist-IDs-in-scrobble-activity.md b/backlog/completed/ml-139 - Polyfill-missing-artist-IDs-in-scrobble-activity.md index f5dad279..203515d0 100644 --- a/backlog/completed/ml-139 - Polyfill-missing-artist-IDs-in-scrobble-activity.md +++ b/backlog/completed/ml-139 - Polyfill-missing-artist-IDs-in-scrobble-activity.md @@ -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 + _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). + diff --git a/backlog/completed/ml-14 - Replace-Process.sleep-with-sys.get_state-in-error_notifier_test.exs.md b/backlog/completed/ml-14 - Replace-Process.sleep-with-sys.get_state-in-error_notifier_test.exs.md index 41eedf09..5be4ed64 100644 --- a/backlog/completed/ml-14 - Replace-Process.sleep-with-sys.get_state-in-error_notifier_test.exs.md +++ b/backlog/completed/ml-14 - Replace-Process.sleep-with-sys.get_state-in-error_notifier_test.exs.md @@ -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 + _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 + + - Zero `Process.sleep` occurrences in `test/` - Tests remain deterministic and pass under repeated runs diff --git a/backlog/completed/ml-140 - Get-artist-images.md b/backlog/completed/ml-140 - Get-artist-images.md index 41c28def..b52cb21a 100644 --- a/backlog/completed/ml-140 - Get-artist-images.md +++ b/backlog/completed/ml-140 - Get-artist-images.md @@ -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 + _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): diff --git a/backlog/completed/ml-141 - Support-records-with-accents.md b/backlog/completed/ml-141 - Support-records-with-accents.md index 2ff861b7..c9385764 100644 --- a/backlog/completed/ml-141 - Support-records-with-accents.md +++ b/backlog/completed/ml-141 - Support-records-with-accents.md @@ -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 + _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. + diff --git a/backlog/completed/ml-142 - Improve-scrobble-UI-in-the-Release-component.md b/backlog/completed/ml-142 - Improve-scrobble-UI-in-the-Release-component.md index 03d1deb8..0f5b0a68 100644 --- a/backlog/completed/ml-142 - Improve-scrobble-UI-in-the-Release-component.md +++ b/backlog/completed/ml-142 - Improve-scrobble-UI-in-the-Release-component.md @@ -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 + 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. + ## Acceptance Criteria + + - [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 + 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. diff --git a/backlog/completed/ml-143 - Cart-style-multi-record-import-in-Add-Record-modal.md b/backlog/completed/ml-143 - Cart-style-multi-record-import-in-Add-Record-modal.md index 9802ea85..b10bf582 100644 --- a/backlog/completed/ml-143 - Cart-style-multi-record-import-in-Add-Record-modal.md +++ b/backlog/completed/ml-143 - Cart-style-multi-record-import-in-Add-Record-modal.md @@ -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 + 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. + ## Acceptance Criteria + + - [ ] #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 diff --git a/backlog/completed/ml-145 - Restructure-scrobble-routes-reuse-Release-component-on-scrobble-page.md b/backlog/completed/ml-145 - Restructure-scrobble-routes-reuse-Release-component-on-scrobble-page.md index d8ba8cc2..3d30586e 100644 --- a/backlog/completed/ml-145 - Restructure-scrobble-routes-reuse-Release-component-on-scrobble-page.md +++ b/backlog/completed/ml-145 - Restructure-scrobble-routes-reuse-Release-component-on-scrobble-page.md @@ -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 + ## 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, ## Acceptance Criteria + + - [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 + ## 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. diff --git a/backlog/completed/ml-146 - Honour-Retry-After-and-rate-limit-reset-headers-for-precise-snooze.md b/backlog/completed/ml-146 - Honour-Retry-After-and-rate-limit-reset-headers-for-precise-snooze.md index 53fecc95..53fd7417 100644 --- a/backlog/completed/ml-146 - Honour-Retry-After-and-rate-limit-reset-headers-for-precise-snooze.md +++ b/backlog/completed/ml-146 - Honour-Retry-After-and-rate-limit-reset-headers-for-precise-snooze.md @@ -4,8 +4,8 @@ title: Honour Retry-After and rate-limit reset headers for precise snooze status: Done assignee: - Codex -created_date: '2026-04-24 11:12' -updated_date: '2026-04-27 21:02' +created_date: "2026-04-24 11:12" +updated_date: "2026-04-27 21:02" labels: [] dependencies: - ML-21 @@ -16,20 +16,21 @@ ordinal: 1000 ## Description + Follow-up to ML-21. Once workers classify API errors as transient vs permanent, the next refinement is using per-response retry hints instead of fixed snooze durations. ## What each API provides -| API | Header | Format | -|-----|--------|--------| -| MusicBrainz | `Retry-After` | seconds (on 503) | -| Wikipedia REST v1 / Action API | `Retry-After` | seconds (on 429 / 503 from `maxlag`) | -| Discogs | `X-Discogs-Ratelimit-Remaining` + rolling 60s window | no `Retry-After`; fallback to a fixed backoff | -| Brave Search | `X-RateLimit-Reset` | seconds until reset (comma-separated for multi-window policies) | -| OpenAI | `x-ratelimit-reset-requests`, `x-ratelimit-reset-tokens` | duration string (e.g. `120ms`, `2s`) | -| Last.fm | none | keep existing `ErrorResponse.retry_delay/1` fallback | +| API | Header | Format | +| ------------------------------ | -------------------------------------------------------- | --------------------------------------------------------------- | +| MusicBrainz | `Retry-After` | seconds (on 503) | +| Wikipedia REST v1 / Action API | `Retry-After` | seconds (on 429 / 503 from `maxlag`) | +| Discogs | `X-Discogs-Ratelimit-Remaining` + rolling 60s window | no `Retry-After`; fallback to a fixed backoff | +| Brave Search | `X-RateLimit-Reset` | seconds until reset (comma-separated for multi-window policies) | +| OpenAI | `x-ratelimit-reset-requests`, `x-ratelimit-reset-tokens` | duration string (e.g. `120ms`, `2s`) | +| Last.fm | none | keep existing `ErrorResponse.retry_delay/1` fallback | ## Why this is a follow-up and not part of ML-21 @@ -45,11 +46,14 @@ ML-21 gets us from "no classification" to `:retry | :cancel`. That alone lets wo ## Dependencies Blocked by ML-21 — requires the classification plumbing to exist first. + ## Acceptance Criteria + -- [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 + # 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 + 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`. + ## Final Summary + ## 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. diff --git a/backlog/completed/ml-147 - Upgrade-to-ServerSentEvents-1.0.md b/backlog/completed/ml-147 - Upgrade-to-ServerSentEvents-1.0.md index fe76b202..605bde7a 100644 --- a/backlog/completed/ml-147 - Upgrade-to-ServerSentEvents-1.0.md +++ b/backlog/completed/ml-147 - Upgrade-to-ServerSentEvents-1.0.md @@ -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 + 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). + ## Acceptance Criteria + + - [ ] #1 Plain refactor - dependency is updated and functionality keeps working as expected diff --git a/backlog/completed/ml-149 - Fix-release-selection-bar-text-visibility-in-Safari.md b/backlog/completed/ml-149 - Fix-release-selection-bar-text-visibility-in-Safari.md index bcec2447..e86e7fac 100644 --- a/backlog/completed/ml-149 - Fix-release-selection-bar-text-visibility-in-Safari.md +++ b/backlog/completed/ml-149 - Fix-release-selection-bar-text-visibility-in-Safari.md @@ -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 + 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. + ## Acceptance Criteria + + - [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 + 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 + 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. + ## Final Summary + 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` diff --git a/backlog/completed/ml-15 - Sanitize-Wikipedia-bio_html-in-ArtistLive.Show.md b/backlog/completed/ml-15 - Sanitize-Wikipedia-bio_html-in-ArtistLive.Show.md index 67cb0883..24cd5cdf 100644 --- a/backlog/completed/ml-15 - Sanitize-Wikipedia-bio_html-in-ArtistLive.Show.md +++ b/backlog/completed/ml-15 - Sanitize-Wikipedia-bio_html-in-ArtistLive.Show.md @@ -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 + _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 + + - Either HTML sanitization applied, or annotation + justification added - Sobelow scan stays clean at `--exit high` diff --git a/backlog/completed/ml-150 - Extract-Records-sub-contexts-to-reduce-module-size.md b/backlog/completed/ml-150 - Extract-Records-sub-contexts-to-reduce-module-size.md index 9a877342..3109be6a 100644 --- a/backlog/completed/ml-150 - Extract-Records-sub-contexts-to-reduce-module-size.md +++ b/backlog/completed/ml-150 - Extract-Records-sub-contexts-to-reduce-module-size.md @@ -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 + 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. + ## Acceptance Criteria + + - [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 + ## 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) diff --git a/backlog/completed/ml-151 - Document-Assets.Cache-TTL-and-cache-invalidation-strategy.md b/backlog/completed/ml-151 - Document-Assets.Cache-TTL-and-cache-invalidation-strategy.md index adcd392b..7967414b 100644 --- a/backlog/completed/ml-151 - Document-Assets.Cache-TTL-and-cache-invalidation-strategy.md +++ b/backlog/completed/ml-151 - Document-Assets.Cache-TTL-and-cache-invalidation-strategy.md @@ -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 + `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 ## Acceptance Criteria + + - [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 + ## 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 diff --git a/backlog/completed/ml-152 - Add-API-version-prefix-to-collection-endpoints.md b/backlog/completed/ml-152 - Add-API-version-prefix-to-collection-endpoints.md index e812b347..3ea18773 100644 --- a/backlog/completed/ml-152 - Add-API-version-prefix-to-collection-endpoints.md +++ b/backlog/completed/ml-152 - Add-API-version-prefix-to-collection-endpoints.md @@ -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 + 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: ## Acceptance Criteria + + - [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 + ## 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 + ## 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 ``` + diff --git a/backlog/completed/ml-153 - Move-MusicBrainz-data-transformations-out-of-Record-schema.md b/backlog/completed/ml-153 - Move-MusicBrainz-data-transformations-out-of-Record-schema.md index 09597eb5..a1594c0c 100644 --- a/backlog/completed/ml-153 - Move-MusicBrainz-data-transformations-out-of-Record-schema.md +++ b/backlog/completed/ml-153 - Move-MusicBrainz-data-transformations-out-of-Record-schema.md @@ -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 + 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 ## Acceptance Criteria + + - [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 + `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. + ## Final Summary + 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. + diff --git a/backlog/completed/ml-154 - Add-early-platform-detection-in-runtime.exs-for-SQLite-extensions.md b/backlog/completed/ml-154 - Add-early-platform-detection-in-runtime.exs-for-SQLite-extensions.md index 52e2ba7a..632b06a7 100644 --- a/backlog/completed/ml-154 - Add-early-platform-detection-in-runtime.exs-for-SQLite-extensions.md +++ b/backlog/completed/ml-154 - Add-early-platform-detection-in-runtime.exs-for-SQLite-extensions.md @@ -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 + `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). + ## Acceptance Criteria + + - [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 diff --git a/backlog/completed/ml-155 - Compact-collection-summary-format-statistical-preamble-for-chat-context.md b/backlog/completed/ml-155 - Compact-collection-summary-format-statistical-preamble-for-chat-context.md index 16238687..6ddffc62 100644 --- a/backlog/completed/ml-155 - Compact-collection-summary-format-statistical-preamble-for-chat-context.md +++ b/backlog/completed/ml-155 - Compact-collection-summary-format-statistical-preamble-for-chat-context.md @@ -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 + 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 + 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. + diff --git a/backlog/completed/ml-157 - Prevent-pi-from-accessing-sensitive-files.md b/backlog/completed/ml-157 - Prevent-pi-from-accessing-sensitive-files.md index c7adf3db..6e97f466 100644 --- a/backlog/completed/ml-157 - Prevent-pi-from-accessing-sensitive-files.md +++ b/backlog/completed/ml-157 - Prevent-pi-from-accessing-sensitive-files.md @@ -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 + 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. + ## Acceptance Criteria + + - [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 + ## 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: " }` (error message for non-interactive mode) @@ -135,25 +139,26 @@ Implementation: **Edge Cases and Accepted Limitations:** -| Scenario | Handling | Rationale | -|----------|----------|-----------| -| `cat ".env"` (quoted) | Caught by path substring scan | Quotes don't hide the path fragment | -| `cat ./.env` (relative with `./`) | Caught by path substring scan | `.env` fragment still present | -| `cat /absolute/path/.env` | Caught by path substring scan | `.env` fragment still present | -| `cat $HOME/project/.env` | Caught (if `.env` appears verbatim) | Variable expansion at start doesn't mask the path fragment | -| `head .env`, `tail .env`, `less .env` | Caught by path substring scan | All contain `.env` fragment | -| `echo $MY_SECRET` (no file access) | NOT caught | This reads from environment memory, not disk. PI would need separate treatment for this (out of scope) | -| `cat $(echo .env)` (command substitution) | NOT caught | Requires shell parsing; accepted limitation for accidental-access use case | -| `eval 'cat .env'` | NOT caught | Requires shell parsing; accepted limitation | -| `sh -c 'cat .env'` | Caught by path substring scan | `.env` appears in the command string | -| Case variants (`.ENV`, `.Env`) | Caught by case-insensitive regex (`i` flag) | macOS filesystems are case-insensitive | -| Path traversal (`../.env`) | Caught by `path.resolve` + `path.normalize` | Resolved absolute path contains `.env` fragment | -| Symlinks (`config -> ~/.aws/`) | NOT caught in initial implementation | Resolving symlinks requires I/O (`realpathSync`) per tool call. Documented limitation; users must not symlink sensitive directories into the project tree. Can be added later via a config option `followSymlinks: true` | -| `/etc/passwd` (system file) | NOT caught unless pattern added to config | Config is the source of truth for blocked paths; system files are not blocked by default | +| Scenario | Handling | Rationale | +| ----------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `cat ".env"` (quoted) | Caught by path substring scan | Quotes don't hide the path fragment | +| `cat ./.env` (relative with `./`) | Caught by path substring scan | `.env` fragment still present | +| `cat /absolute/path/.env` | Caught by path substring scan | `.env` fragment still present | +| `cat $HOME/project/.env` | Caught (if `.env` appears verbatim) | Variable expansion at start doesn't mask the path fragment | +| `head .env`, `tail .env`, `less .env` | Caught by path substring scan | All contain `.env` fragment | +| `echo $MY_SECRET` (no file access) | NOT caught | This reads from environment memory, not disk. PI would need separate treatment for this (out of scope) | +| `cat $(echo .env)` (command substitution) | NOT caught | Requires shell parsing; accepted limitation for accidental-access use case | +| `eval 'cat .env'` | NOT caught | Requires shell parsing; accepted limitation | +| `sh -c 'cat .env'` | Caught by path substring scan | `.env` appears in the command string | +| Case variants (`.ENV`, `.Env`) | Caught by case-insensitive regex (`i` flag) | macOS filesystems are case-insensitive | +| Path traversal (`../.env`) | Caught by `path.resolve` + `path.normalize` | Resolved absolute path contains `.env` fragment | +| Symlinks (`config -> ~/.aws/`) | NOT caught in initial implementation | Resolving symlinks requires I/O (`realpathSync`) per tool call. Documented limitation; users must not symlink sensitive directories into the project tree. Can be added later via a config option `followSymlinks: true` | +| `/etc/passwd` (system file) | NOT caught unless pattern added to config | Config is the source of truth for blocked paths; system files are not blocked by default | -**Bash scanning scope:** The bash scanner uses simple substring/regex matching on the raw command string. It is designed to catch *accidental* access by the LLM, not adversarial bypass by a human. Commands using shell variable expansion, command substitution, or indirect execution may evade detection; this is an accepted limitation documented above. +**Bash scanning scope:** The bash scanner uses simple substring/regex matching on the raw command string. It is designed to catch _accidental_ access by the LLM, not adversarial bypass by a human. Commands using shell variable expansion, command substitution, or indirect execution may evade detection; this is an accepted limitation documented above. Verification: + - Start pi in this project, ask it to read `.env.example` (should work — this is NOT a secret, it's the example) - Ask pi to read `.env` (should be blocked with a notification) - Ask pi to run `cat .env` via bash (should be blocked) @@ -211,6 +216,7 @@ Verification: All tests pass before proceeding to integration verification. The extension must handle `ctx.hasUI === false` (print mode, JSON mode) by returning a descriptive error reason string, so the blocked access is reported to stdout rather than silently swallowed. Verification: + ```bash pi -p "read the .env file" 2>&1 | grep -i "blocked" # Should produce output indicating the access was blocked @@ -219,6 +225,7 @@ pi -p "read the .env file" 2>&1 | grep -i "blocked" **Step 4: Verify other tools are unaffected** Ask pi to: + - Read a normal source file (e.g., `lib/music_library.ex`) - Edit a normal file - Run `mix test` @@ -232,41 +239,41 @@ Verification: All normal pi operations are unaffected. Each step includes concrete verification instructions above. Overall verification suite: -| Test Case | Tool | Target | Expected | -|-----------|------|--------|----------| -| Read .env | read | `.env` | Blocked | -| Read .env via path traversal | read | `../.env` | Blocked | -| Read .env.production | read | `.env.production` | Blocked (if pattern added) | -| Read .env.example | read | `.env.example` | **Allowed** (example file) | -| Read .ENV (case variant) | read | `.ENV` | Blocked (case-insensitive) | -| Read secrets file | read | `config/secrets.yml` | Blocked | -| Cat .env via bash | bash | `cat .env` | Blocked | -| Cat .env via bash with quotes | bash | `cat ".env"` | Blocked | -| Cat ./.env via bash | bash | `cat ./.env` | Blocked | -| Grep in .ssh | grep | `~/.ssh/config` | Blocked | -| Ls in .aws | ls | `~/.aws/` | Blocked | -| Write to .env | write | `.env` | Blocked | -| Edit .env | edit | `.env` | Blocked | -| Bash printenv (no path) | bash | `printenv` | Blocked | -| Bash env command | bash | `env` | Blocked | -| Bash echo $NODE_ENV (false positive check) | bash | `echo $NODE_ENV` | **Allowed** (`\b` anchor) | -| Read normal .ex file | read | `lib/music_library.ex` | Allowed | -| Run mix test | bash | `mix test` | Allowed | -| Non-interactive read .env | read | `.env` (print mode) | Error reported | -| Non-interactive read source file | read | `lib/music_library.ex` (print mode) | Normal output | -| Add pattern + /reload | read | newly blocked path | Blocked after reload | +| Test Case | Tool | Target | Expected | +| ------------------------------------------ | ----- | ----------------------------------- | -------------------------- | +| Read .env | read | `.env` | Blocked | +| Read .env via path traversal | read | `../.env` | Blocked | +| Read .env.production | read | `.env.production` | Blocked (if pattern added) | +| Read .env.example | read | `.env.example` | **Allowed** (example file) | +| Read .ENV (case variant) | read | `.ENV` | Blocked (case-insensitive) | +| Read secrets file | read | `config/secrets.yml` | Blocked | +| Cat .env via bash | bash | `cat .env` | Blocked | +| Cat .env via bash with quotes | bash | `cat ".env"` | Blocked | +| Cat ./.env via bash | bash | `cat ./.env` | Blocked | +| Grep in .ssh | grep | `~/.ssh/config` | Blocked | +| Ls in .aws | ls | `~/.aws/` | Blocked | +| Write to .env | write | `.env` | Blocked | +| Edit .env | edit | `.env` | Blocked | +| Bash printenv (no path) | bash | `printenv` | Blocked | +| Bash env command | bash | `env` | Blocked | +| Bash echo $NODE_ENV (false positive check) | bash | `echo $NODE_ENV` | **Allowed** (`\b` anchor) | +| Read normal .ex file | read | `lib/music_library.ex` | Allowed | +| Run mix test | bash | `mix test` | Allowed | +| Non-interactive read .env | read | `.env` (print mode) | Error reported | +| Non-interactive read source file | read | `lib/music_library.ex` (print mode) | Normal output | +| Add pattern + /reload | read | newly blocked path | Blocked after reload | ### 5. Architecture Impact Analysis -| Touchpoint | Impact | -|------------|--------| +| Touchpoint | Impact | +| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `.pi/extensions/sensitive-file-guard.ts` | **New file** — extension entry point. Named `000-sensitive-file-guard.ts` (or similar numeric prefix) to ensure it loads **first** among project-local extensions. Since `tool_call` handlers chain in load order and later handlers can mutate `event.input` before this guard sees it, loading first guarantees the guard inspects the original, unmodified tool arguments. | -| `.pi/sensitive-paths.json` | **New file** — declarative blocked patterns config | -| `tool_call` event | New subscriber, chains with existing extensions (e.g., MCP adapter). The guard runs **before** the MCP adapter's `tool_call` handler, so sensitive file access is blocked before any MCP forwarding occurs. | -| Built-in tools | **Unchanged** — interception is non-invasive | -| Phoenix/Ecto/Oban | **No impact** — pi-only concern, not part of the Elixir application | -| Production infrastructure | **No impact** — this is developer-local tooling, not server configuration | -| Other pi extensions (MCP adapter) | Compatible — `tool_call` handlers chain in load order. The guard runs alongside, not instead of. | +| `.pi/sensitive-paths.json` | **New file** — declarative blocked patterns config | +| `tool_call` event | New subscriber, chains with existing extensions (e.g., MCP adapter). The guard runs **before** the MCP adapter's `tool_call` handler, so sensitive file access is blocked before any MCP forwarding occurs. | +| Built-in tools | **Unchanged** — interception is non-invasive | +| Phoenix/Ecto/Oban | **No impact** — pi-only concern, not part of the Elixir application | +| Production infrastructure | **No impact** — this is developer-local tooling, not server configuration | +| Other pi extensions (MCP adapter) | Compatible — `tool_call` handlers chain in load order. The guard runs alongside, not instead of. | **No migration or deprecation path needed** — this is a net-new capability. @@ -283,6 +290,7 @@ Each step includes concrete verification instructions above. Overall verificatio ### 7. Benchmarking Requirements **No benchmarks needed.** The guard is a simple gate with constant-time pattern matching: + - O(20) regex tests per tool call - No I/O beyond the initial config read (done once at startup) - No allocations beyond the return value object @@ -293,6 +301,7 @@ If future patterns grow to hundreds of entries, a trie-based matcher could be co ### 8. Cost Profile **Zero cost.** The guard: + - Uses no third-party APIs - Consumes no paid resources - Runs entirely locally in the pi process @@ -314,6 +323,7 @@ If future patterns grow to hundreds of entries, a trie-based matcher could be co ### 10. Documentation Updates **`docs/project-conventions.md`** — Add a section "Pi Security Configuration" documenting: + - The existence of the sensitive file guard extension - The location and format of `.pi/sensitive-paths.json` - How to add or remove blocked patterns @@ -329,11 +339,13 @@ If future patterns grow to hundreds of entries, a trie-based matcher could be co **SKILL.md (optional)** — Consider creating `.claude/skills/sensitive-file-guard/SKILL.md` so pi itself understands the guard's behavior. Without this, pi may try to read a blocked file, receive the block reason in the next turn, and need to re-plan. A SKILL.md pre-loads this knowledge so pi avoids attempting blocked paths proactively. This is a nice-to-have, not required for the initial implementation. **No other documentation files need updates.** This is a pi-level concern, not an application architecture concern. + ## Implementation Notes + ## 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. ## Final Summary + 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. + diff --git a/backlog/completed/ml-158 - Force-production-logs-to-single-line-format.md b/backlog/completed/ml-158 - Force-production-logs-to-single-line-format.md index 115a15aa..700f77a6 100644 --- a/backlog/completed/ml-158 - Force-production-logs-to-single-line-format.md +++ b/backlog/completed/ml-158 - Force-production-logs-to-single-line-format.md @@ -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 + 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. + ## Acceptance Criteria + + - [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 + ## Implementation Plan: Force production logs to single-line format ### Objective alignment @@ -48,6 +53,7 @@ When running in production, logs spanning multiple lines create issues — they The problem: multi-line log output in production prevents reliable line-based filtering and makes log output impossible to reverse. Log sources include HTTP request logs (two separate `Logger.info` calls from `Phoenix.Logger`), LiveView handshake logs (single `Logger.info` with embedded newlines), and any custom `Logger` calls that pass multi-line strings. The solution has three layers: + 1. **Logster** replaces `Phoenix.Logger` for HTTP request logging — merges `GET + Sent` into one logfmt line 2. **Custom telemetry handler** replaces `Phoenix.Logger`'s `[:phoenix, :socket_connected]` handler — flattens LiveView handshake into one line 3. **Custom Logger.Formatter** acts as a universal safety net — replaces any remaining embedded newlines with escaped `\n` in ALL log messages @@ -72,16 +78,16 @@ Before writing code, verify two assumptions: `Phoenix.Logger` auto-attaches to 8 telemetry events. With production log level `:info`, only these produce visible output: -| Event | Level | Multi-line? | Covered by | -|---|---|---|---| -| `[:phoenix, :endpoint, :start]` | `:info` | No (one line) | Logster v2 | -| `[:phoenix, :endpoint, :stop]` | `:info` | No (one line) | Logster v2 | -| `[:phoenix, :socket_connected]` | `:info` | **Yes** (4+ lines) | Custom handler (Step 4) | -| `[:phoenix, :error_rendered]` | `:error` | No (one line) | Silenced — acceptable (ErrorTracker already captures errors) | -| `[:phoenix, :router_dispatch, :start]` | `:debug` | Yes | Already filtered at `:info` level — no action needed | -| `[:phoenix, :socket_drain]` | `:debug` | No | Already filtered | -| `[:phoenix, :channel_joined]` | `:debug` | Yes | Already filtered (LiveView uses `"lv:"` topics not `"phoenix"` internal topics, but default log_join level is `:debug`) | -| `[:phoenix, :channel_handled_in]` | `:debug` | Yes | Already filtered | +| Event | Level | Multi-line? | Covered by | +| -------------------------------------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------- | +| `[:phoenix, :endpoint, :start]` | `:info` | No (one line) | Logster v2 | +| `[:phoenix, :endpoint, :stop]` | `:info` | No (one line) | Logster v2 | +| `[:phoenix, :socket_connected]` | `:info` | **Yes** (4+ lines) | Custom handler (Step 4) | +| `[:phoenix, :error_rendered]` | `:error` | No (one line) | Silenced — acceptable (ErrorTracker already captures errors) | +| `[:phoenix, :router_dispatch, :start]` | `:debug` | Yes | Already filtered at `:info` level — no action needed | +| `[:phoenix, :socket_drain]` | `:debug` | No | Already filtered | +| `[:phoenix, :channel_joined]` | `:debug` | Yes | Already filtered (LiveView uses `"lv:"` topics not `"phoenix"` internal topics, but default log_join level is `:debug`) | +| `[:phoenix, :channel_handled_in]` | `:debug` | Yes | Already filtered | **Verdict**: The only events that produce visible output at `:info`+ are endpoint start/stop, socket_connected, and error_rendered. Disabling Phoenix.Logger is safe — the first two are replaced by Logster, socket_connected is replaced by the custom handler, and losing error_rendered is acceptable because ErrorTracker already captures all errors via its own telemetry listener. @@ -94,6 +100,7 @@ After adding the Logster dependency (Step 1), inspect `Logster`'s telemetry atta **Verify**: In an IEx session: `Logster.__info__(:functions)` or check the Logster source to see which events it attaches to. If it covers `socket_connected`, **skip Step 4** — the custom telemetry handler is unnecessary. #### Step 1: Add Logster dependency + - Add `{:logster, "~> 2.0.0-rc.5"}` to `mix.exs` deps - Run `mix deps.get` - **Verify**: `mix compile` succeeds, `Logster` module is available @@ -222,18 +229,18 @@ After adding the Logster dependency (Step 1), inspect `Logster`'s telemetry atta ### Verifiability -| Step | Verification | -|---|---| -| 0a | Inspect stdout with `MIX_ENV=prod mix phx.server` before any changes — confirm only endpoint start/stop, socket_connected, and error_rendered appear at `:info`+ | -| 0b | Inspect Logster source in `deps/logster/` — check if `socket_connected` is handled; if yes, skip Step 4 | -| 1 | `mix compile` passes, `Logster` module is available in IEx | -| 2-6 | Start app with `MIX_ENV=prod mix phx.server`, hit endpoints, inspect stdout — all logs on one line | -| 4 | Visit a LiveView page (`/collection`) in prod mode, verify single-line output in stdout | -| 5 | `mix test test/music_library/logger/single_line_formatter_test.exs` — all tests pass | -| 7 | Both `rg` searches (escaped `\n` and multi-line Logger calls) return empty for `lib/` | -| 8 | `mix test` passes with ≥75% coverage; dev env test confirms multi-line output unchanged | -| 9 | `MIX_ENV=prod mix release` succeeds; release binary starts without errors; log output is single-line | -| 10 | Read both docs, confirm accuracy against implemented config and modules | +| Step | Verification | +| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 0a | Inspect stdout with `MIX_ENV=prod mix phx.server` before any changes — confirm only endpoint start/stop, socket_connected, and error_rendered appear at `:info`+ | +| 0b | Inspect Logster source in `deps/logster/` — check if `socket_connected` is handled; if yes, skip Step 4 | +| 1 | `mix compile` passes, `Logster` module is available in IEx | +| 2-6 | Start app with `MIX_ENV=prod mix phx.server`, hit endpoints, inspect stdout — all logs on one line | +| 4 | Visit a LiveView page (`/collection`) in prod mode, verify single-line output in stdout | +| 5 | `mix test test/music_library/logger/single_line_formatter_test.exs` — all tests pass | +| 7 | Both `rg` searches (escaped `\n` and multi-line Logger calls) return empty for `lib/` | +| 8 | `mix test` passes with ≥75% coverage; dev env test confirms multi-line output unchanged | +| 9 | `MIX_ENV=prod mix release` succeeds; release binary starts without errors; log output is single-line | +| 10 | Read both docs, confirm accuracy against implemented config and modules | ### Architecture impact analysis @@ -252,16 +259,19 @@ After adding the Logster dependency (Step 1), inspect `Logster`'s telemetry atta **UI components**: None affected. **Config changes**: + - `config/config.exs`: add `config :music_library, :single_line_logging, false` - `mix.exs`: new dependency `{:logster, "~> 2.0.0-rc.5"}` - `config/prod.exs`: 4 additions (phoenix logger disable, single_line_logging flag, logster config, formatter config) - `lib/music_library/application.ex`: 1 conditional block (Logster attach + optional telemetry handler attach) **New modules**: + - `lib/music_library/logger/single_line_formatter.ex` — Logger.Formatter format/4 function; requires `@moduledoc` (Credo strict mode) - `lib/music_library_web/telemetry/log_handler.ex` — Phoenix socket telemetry handler (conditional on Step 0b); requires `@moduledoc` **New tests**: + - `test/music_library/logger/single_line_formatter_test.exs` — unit tests for the formatter (newline replacement, iolist handling, metadata preservation) - `test/music_library_web/telemetry/log_handler_test.exs` — unit tests for the telemetry handler (single-line output, param filtering) - Integration assertions in existing or new test files for dev config unchanged @@ -273,6 +283,7 @@ After adding the Logster dependency (Step 1), inspect `Logster`'s telemetry atta ### Performance profile **Runtime complexity**: O(1) per log event. + - Logster: string interpolation from telemetry metadata per HTTP request - Custom telemetry: same — one function call per socket connection - Custom formatter: one `IO.chardata_to_string/1` + `String.replace/3` per log event (message size bounded by Logger truncation) @@ -288,13 +299,15 @@ After adding the Logster dependency (Step 1), inspect `Logster`'s telemetry atta ### Benchmarking requirements None needed. The operations are: + - String replacement on logger-truncated messages (bounded by default 4KB or configurable) - String interpolation from telemetry metadata (no IO, no computation) -These are trivially fast. If future log volume increases by orders of magnitude, the `Logger` overload protection (message dropping at >500/sec) will engage before our formatter becomes a bottleneck. + These are trivially fast. If future log volume increases by orders of magnitude, the `Logger` overload protection (message dropping at >500/sec) will engage before our formatter becomes a bottleneck. ### Cost profile No paid resources consumed. + - Logster: MIT license, free - No API calls, no compute, no storage costs - No third-party services @@ -314,26 +327,32 @@ No manual production changes required. All configuration is in `config/prod.exs` **Rollout**: Standard deploy (push to main → GitHub Actions → Coolify). First deploy will be slower due to new dependency compilation (Logster). Subsequent deploys use cached Docker layer. **Rollback**: Revert to previous commit. No data migration needed. + ## Final Summary + ## 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) diff --git a/backlog/completed/ml-159 - select-and-copy-log-lines-from-log-browser.md b/backlog/completed/ml-159 - select-and-copy-log-lines-from-log-browser.md index 29be8f2a..1e421a89 100644 --- a/backlog/completed/ml-159 - select-and-copy-log-lines-from-log-browser.md +++ b/backlog/completed/ml-159 - select-and-copy-log-lines-from-log-browser.md @@ -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 + 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`. 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). + ## Acceptance Criteria + + - [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`. To return copied text, the ## Implementation Plan + ## Implementation Plan: Vim-style Visual Mode + setEditorText ### 1. Add cursor state to LogViewer @@ -55,6 +60,7 @@ Add `cursorIndex` (absolute index into `this.lines`) and initialize to `0`. The **Navigation model (vim-style):** All movement keys (`j`, `k`, `PgUp`, `PgDn`, `Home`, `End`, `g`, `G`) move `cursorIndex`. The `scrollOffset` auto-adjusts via a `clampViewport()` helper to keep the cursor within the visible range `[scrollOffset, scrollOffset + visibleHeight)`. This unifies normal and visual mode key handling — in visual mode, the same keys extend the selection by moving `cursorIndex` while `visualAnchor` stays fixed. Add a `clampViewport()` private method: + - If `cursorIndex < scrollOffset`, set `scrollOffset = cursorIndex` - If `cursorIndex >= scrollOffset + visibleHeight`, set `scrollOffset = cursorIndex - visibleHeight + 1` - Call `clampViewport()` after every cursor movement and after `updateLines()` @@ -74,9 +80,9 @@ In visual mode, all movement keys (`j`, `k`, `PgUp`, `PgDn`, `Home`, `End`, `g`, ### 3. Update render to show cursor highlight and selection range - Cursor line: `> ` prefix with `theme.fg("accent", ...)` -- Selected lines (in range): `● ` prefix with `theme.fg("success", ...)` +- Selected lines (in range): `● ` prefix with `theme.fg("success", ...)` - Lines that are both cursor AND in selection: `> ` prefix takes priority (cursor indicator) -- Non-cursor, non-selected: keep existing ` NNN │ ` prefix +- Non-cursor, non-selected: keep existing `NNN │` prefix The line number column must shift right by 2 characters to accommodate the new prefix: ` NNN │ > text` instead of ` NNN │ text`. @@ -93,11 +99,13 @@ The line number column must shift right by 2 characters to accommodate the new p ### 5. Change ctx.ui.custom return type from void to string | null ```typescript -const copiedText = await ctx.ui.custom((tui, theme, _kb, done) => { - viewer.onCopy = (text: string) => done(text); - viewer.onClose = () => done(null); - // ... -}); +const copiedText = await ctx.ui.custom( + (tui, theme, _kb, done) => { + viewer.onCopy = (text: string) => done(text); + viewer.onClose = () => done(null); + // ... + }, +); if (copiedText !== null) { ctx.ui.setEditorText(copiedText); @@ -130,16 +138,17 @@ This is a **single-file, frontend-only change** to `.pi/extensions/prod-logs/ind ### Touchpoints -| Component | Impact | -|-----------|--------| -| `LogViewer` class | **Modified** — adds `cursorIndex`, `visualMode`, `visualAnchor`, `onCopy` callback, `clampViewport()` private method. Movement keys refactored to move `cursorIndex` with viewport auto-clamping. `handleInput` routing restructured for mode awareness | -| `LogViewer.render()` | **Modified** — adds cursor prefix (`> `) and selection prefix (`● `). Line number column padding widened by 2 chars | -| `LogViewer.updateLines()` | **Modified** — resets `cursorIndex = 0`, `visualMode = false`, `visualAnchor = 0`, calls `clampViewport()` | -| Extension handler function | **Modified** — `ctx.ui.custom` → `ctx.ui.custom`. Adds `onCopy` handler. Adds `ctx.ui.setEditorText()` on copy | -| Help text strings | **Modified** — split into normal-mode and visual-mode variants | -| `fetchLogs` / Coolify API | **No change** — data fetching unchanged | +| Component | Impact | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `LogViewer` class | **Modified** — adds `cursorIndex`, `visualMode`, `visualAnchor`, `onCopy` callback, `clampViewport()` private method. Movement keys refactored to move `cursorIndex` with viewport auto-clamping. `handleInput` routing restructured for mode awareness | +| `LogViewer.render()` | **Modified** — adds cursor prefix (`> `) and selection prefix (`● `). Line number column padding widened by 2 chars | +| `LogViewer.updateLines()` | **Modified** — resets `cursorIndex = 0`, `visualMode = false`, `visualAnchor = 0`, calls `clampViewport()` | +| Extension handler function | **Modified** — `ctx.ui.custom` → `ctx.ui.custom`. Adds `onCopy` handler. Adds `ctx.ui.setEditorText()` on copy | +| Help text strings | **Modified** — split into normal-mode and visual-mode variants | +| `fetchLogs` / Coolify API | **No change** — data fetching unchanged | ### Deprecation/Migration + None. Old behavior (scroll + refresh + close) is preserved and extended, not replaced. --- @@ -174,18 +183,20 @@ None. Old behavior (scroll + refresh + close) is preserved and extended, not rep ## Documentation Updates -| File | Change Needed | -|------|--------------| -| `docs/architecture.md` | **No change** — this is a pi extension, not part of the Elixir application architecture | -| `docs/project-conventions.md` | **No change** — TypeScript conventions are pi's domain, not the project's | -| `docs/production-infrastructure.md` | **No change** — no infra changes | -| `docs/available-tasks.md` | **No change** — no new mise tasks | +| File | Change Needed | +| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `docs/architecture.md` | **No change** — this is a pi extension, not part of the Elixir application architecture | +| `docs/project-conventions.md` | **No change** — TypeScript conventions are pi's domain, not the project's | +| `docs/production-infrastructure.md` | **No change** — no infra changes | +| `docs/available-tasks.md` | **No change** — no new mise tasks | | `.pi/extensions/prod-logs/index.ts` | **Inline comments** — add JSDoc on new fields (`cursorIndex`, `visualMode`, `visualAnchor`, `onCopy`) and new key binding branches | + ## Final Summary + 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. + diff --git a/backlog/completed/ml-164 - Create-pi-extension-for-interactive-error-browsing.md b/backlog/completed/ml-164 - Create-pi-extension-for-interactive-error-browsing.md index 22c65250..c624e3f9 100644 --- a/backlog/completed/ml-164 - Create-pi-extension-for-interactive-error-browsing.md +++ b/backlog/completed/ml-164 - Create-pi-extension-for-interactive-error-browsing.md @@ -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 + 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. + ## Implementation Plan + ## Implementation Plan ### Objective alignment @@ -106,14 +109,14 @@ Key fields used by the list view: `id`, `kind`, `reason`, `source_line`, `source ### Architecture impact analysis -| Touchpoint | Impact | -|---|---| -| `.pi/extensions/prod-errors/index.ts` | **New file** — ~400 lines: `ErrorBrowser` class, helpers, command registration | -| `.pi/extensions/prod-errors/package.json` | **New file** — minimal `{ name, private, description }` | -| `.pi/extensions/prod-logs/index.ts` | **No change** | -| All Elixir modules, router, PubSub, supervision tree | **No change** — purely a pi extension | -| Pi env vars | Reuses `PI_API_TOKEN` and `PI_SERVICE_FQDN_WEB` from ML-163 | -| Existing pi extensions | **No change** — `/prod-errors` is a new command | +| Touchpoint | Impact | +| ---------------------------------------------------- | ------------------------------------------------------------------------------ | +| `.pi/extensions/prod-errors/index.ts` | **New file** — ~400 lines: `ErrorBrowser` class, helpers, command registration | +| `.pi/extensions/prod-errors/package.json` | **New file** — minimal `{ name, private, description }` | +| `.pi/extensions/prod-logs/index.ts` | **No change** | +| All Elixir modules, router, PubSub, supervision tree | **No change** — purely a pi extension | +| Pi env vars | Reuses `PI_API_TOKEN` and `PI_SERVICE_FQDN_WEB` from ML-163 | +| Existing pi extensions | **No change** — `/prod-errors` is a new command | ### Performance profile @@ -141,8 +144,13 @@ No paid resources. Makes HTTP requests to the project's own server. No third-par #### Step 1: Create `.pi/extensions/prod-errors/package.json` Minimal `package.json` matching the prod-logs pattern: + ```json -{ "name": "prod-errors", "private": true, "description": "Interactive TUI for browsing production errors" } +{ + "name": "prod-errors", + "private": true, + "description": "Interactive TUI for browsing production errors" +} ``` **Verification**: `ls -la .pi/extensions/prod-errors/package.json` — file must exist with valid JSON. @@ -192,6 +200,7 @@ A class managing list view state and rendering, following the `LogViewer` patter **Caching**: Cache rendered lines when `width` and state unchanged. Invalidate on any state mutation. **Verification** (requires ML-162 API running locally): + 1. `/reload` → `/prod-errors` → TUI opens with error list (or empty state). 2. `j`/`k` moves cursor. `r` toggles resolved. `m` toggles muted. `l` loads next page. 3. `Enter` triggers detail fetch. `q`/`Escape` closes TUI. @@ -210,6 +219,7 @@ Extend `ErrorBrowser` with detail rendering. On Enter in list: set `mode = "load **Loading state**: Inline "Loading…" overlay centered in viewport when `mode === "loading"`. **Verification**: + 1. Enter on error → detail shows all sections + occurrences with stacktraces. 2. `j`/`k` scrolls detail. `PgUp`/`PgDn` pages. `Escape` returns to list with cursor preserved. 3. Enter on detail line → line copied to editor (prod-logs pattern). @@ -220,6 +230,7 @@ Extend `ErrorBrowser` with detail rendering. On Enter in list: set `mode = "load #### Step 5: Register the `/prod-errors` command Register via `pi.registerCommand("prod-errors", ...)`. Handler flow: + 1. Validate `PI_SERVICE_FQDN_WEB` and `PI_API_TOKEN` — notify and return if missing. 2. Show `BorderedLoader` while calling `fetchErrors({ limit: 50, offset: 0 })`. 3. On null/empty: notify and return. @@ -229,6 +240,7 @@ Register via `pi.registerCommand("prod-errors", ...)`. Handler flow: In-browser fetches (filter toggles, load more, detail) use an inline loading indicator, not nested `BorderedLoader`. **Verification**: + 1. `/prod-errors` → loader → browser. Toggle filters → inline loading. Load more → inline loading. Detail → inline loading. 2. Bad `PI_SERVICE_FQDN_WEB` → useful error notification. All keyboard shortcuts work. @@ -236,25 +248,26 @@ In-browser fetches (filter toggles, load more, detail) use an inline loading ind #### Step 6: Edge cases and error handling -| Scenario | Handling | -|---|---| -| Empty list (API returned zero errors) | "No production errors found" in TUI | +| Scenario | Handling | +| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Empty list (API returned zero errors) | "No production errors found" in TUI | | All errors filtered out by current toggles | "No errors match the current filters" (only shown when the unfiltered list was non-empty; track with a `totalUnfiltered` field set on the initial fetch) | -| API non-2xx | Error notification with status code; keep TUI open | -| Unexpected JSON shape | Catch TypeError, "Unexpected API response format" notification | -| Network timeout/refused | Catch fetch error, show notification | -| Double Enter while loading | Ignore (`mode === "loading"` guard) | -| Rapid filter toggles | Abort in-flight request via AbortController before new fetch | -| Filter changes invalidate pages | Reset offset to 0, replace errors with first page | -| Load more at end | Show "— end of results —", ignore `l` | -| Zero occurrences | "No occurrences recorded" | -| Empty context/breadcrumbs | Omit those sections | -| Missing stacktrace fields | Show "—" | -| Narrow terminal (< 40 cols) | Render with heavy truncation (acceptable) | +| API non-2xx | Error notification with status code; keep TUI open | +| Unexpected JSON shape | Catch TypeError, "Unexpected API response format" notification | +| Network timeout/refused | Catch fetch error, show notification | +| Double Enter while loading | Ignore (`mode === "loading"` guard) | +| Rapid filter toggles | Abort in-flight request via AbortController before new fetch | +| Filter changes invalidate pages | Reset offset to 0, replace errors with first page | +| Load more at end | Show "— end of results —", ignore `l` | +| Zero occurrences | "No occurrences recorded" | +| Empty context/breadcrumbs | Omit those sections | +| Missing stacktrace fields | Show "—" | +| Narrow terminal (< 40 cols) | Render with heavy truncation (acceptable) | **Abort pattern**: Store `currentAbortController` reference; call `.abort()` before each new fetch; catch `AbortError` and return null. **Verification**: + 1. **Rapid filter toggle**: Press `r` 5 times rapidly → observe only the last fetch's results are displayed (previous requests were aborted). Requests aborted via `AbortController` should not update state. 2. **End-of-results**: Navigate to the last page with `l`, then press `l` again → observe "— end of results —" divider and subsequent `l` presses are ignored. 3. **Empty context/breadcrumbs**: View a detail for an error that has occurrences with `context: {}` and `breadcrumbs: []` → observe those section headings are entirely omitted from the detail rendering. @@ -291,6 +304,7 @@ No new changes beyond ML-162/ML-163: `PI_API_TOKEN` and `PI_SERVICE_FQDN_WEB` ar ## Implementation Notes + ## Implementation Notes ### What was built diff --git a/backlog/completed/ml-17 - Experimental-comment-left-in-Last.fm-API-client.md b/backlog/completed/ml-17 - Experimental-comment-left-in-Last.fm-API-client.md index ce2a5d9b..a318cb98 100644 --- a/backlog/completed/ml-17 - Experimental-comment-left-in-Last.fm-API-client.md +++ b/backlog/completed/ml-17 - Experimental-comment-left-in-Last.fm-API-client.md @@ -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 + _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 + + - No experimental/temporary comments remain in production code - Timeout configuration is intentional and documented diff --git a/backlog/completed/ml-2 - Verify-or-drop-multipart-hex-dependency.md b/backlog/completed/ml-2 - Verify-or-drop-multipart-hex-dependency.md index de2a1f8a..e5a9d119 100644 --- a/backlog/completed/ml-2 - Verify-or-drop-multipart-hex-dependency.md +++ b/backlog/completed/ml-2 - Verify-or-drop-multipart-hex-dependency.md @@ -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 + _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_ ## Acceptance Criteria + + - [ ] #1 Decision recorded: either dep removed, or dep retained with comment explaining transitive need diff --git a/backlog/completed/ml-21 - Classify-API-errors-as-transient-vs-permanent-in-workers.md b/backlog/completed/ml-21 - Classify-API-errors-as-transient-vs-permanent-in-workers.md index b535dd42..d80eff6f 100644 --- a/backlog/completed/ml-21 - Classify-API-errors-as-transient-vs-permanent-in-workers.md +++ b/backlog/completed/ml-21 - Classify-API-errors-as-transient-vs-permanent-in-workers.md @@ -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 + _GitHub: created 2026-04-05 · updated 2026-04-12 · re-scoped 2026-04-24 after per-API research_ ## Summary @@ -31,15 +32,15 @@ The original issue pointed at `LastFm.API.ErrorResponse` as the pattern to copy ## What each API actually returns -| API | Error channel | Rate limit status | Body shape | Retry hint | -|-----|---------------|-------------------|------------|------------| -| Last.fm | HTTP 200 + body | code `29` in body | `{"error": N, "message": "..."}` | none (already handled) | -| MusicBrainz | HTTP status | **503** (not 429) | `{"error": "string"}` | `Retry-After` | -| Discogs | HTTP status | 429 | `{"message": "..."}` | `X-Discogs-Ratelimit-*`, no `Retry-After` | -| Wikipedia REST v1 | HTTP status | 429 | `{"httpCode", "messageTranslations"}` | `Retry-After` | -| Wikipedia/Wikidata Action API | **HTTP 200 + body** (Last.fm-shaped) | 429 or 503 | `{"error": {"code", "info"}}` | `Retry-After` on 503 | -| Brave Search | HTTP status | 429 | `{"type": "ErrorResponse", "error": {"status", "code", "detail"}}` | `X-RateLimit-Reset` (seconds) | -| OpenAI | HTTP status + body `code` | 429 for **both** rate limit and quota | `{"error": {"type", "code", "message", "param"}}` | `x-ratelimit-reset-*` | +| API | Error channel | Rate limit status | Body shape | Retry hint | +| ----------------------------- | ------------------------------------ | ------------------------------------- | ------------------------------------------------------------------ | ----------------------------------------- | +| Last.fm | HTTP 200 + body | code `29` in body | `{"error": N, "message": "..."}` | none (already handled) | +| MusicBrainz | HTTP status | **503** (not 429) | `{"error": "string"}` | `Retry-After` | +| Discogs | HTTP status | 429 | `{"message": "..."}` | `X-Discogs-Ratelimit-*`, no `Retry-After` | +| Wikipedia REST v1 | HTTP status | 429 | `{"httpCode", "messageTranslations"}` | `Retry-After` | +| Wikipedia/Wikidata Action API | **HTTP 200 + body** (Last.fm-shaped) | 429 or 503 | `{"error": {"code", "info"}}` | `Retry-After` on 503 | +| Brave Search | HTTP status | 429 | `{"type": "ErrorResponse", "error": {"status", "code", "detail"}}` | `X-RateLimit-Reset` (seconds) | +| OpenAI | HTTP status + body `code` | 429 for **both** rate limit and quota | `{"error": {"type", "code", "message", "param"}}` | `x-ratelimit-reset-*` | ## Real quirks to encode (not 14 error atoms) @@ -69,7 +70,9 @@ Out of scope (tracked separately): honouring `Retry-After` / `X-*-Reset` headers ## Acceptance Criteria + + - [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 + ## 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). + diff --git a/backlog/completed/ml-22 - Search-count-function-pairs-duplicate-query-logic.md b/backlog/completed/ml-22 - Search-count-function-pairs-duplicate-query-logic.md index 1d83445b..caf3fd3c 100644 --- a/backlog/completed/ml-22 - Search-count-function-pairs-duplicate-query-logic.md +++ b/backlog/completed/ml-22 - Search-count-function-pairs-duplicate-query-logic.md @@ -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 + _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 + + - Query logic is defined once per search domain - No regression in search results or counts diff --git a/backlog/completed/ml-24 - RecordComponents-is-755-lines-with-40-mixed-functions.md b/backlog/completed/ml-24 - RecordComponents-is-755-lines-with-40-mixed-functions.md index 476513e8..c4f806d9 100644 --- a/backlog/completed/ml-24 - RecordComponents-is-755-lines-with-40-mixed-functions.md +++ b/backlog/completed/ml-24 - RecordComponents-is-755-lines-with-40-mixed-functions.md @@ -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 + _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 + + - Each module has a clear, focused purpose - Shared action menu markup lives in one place - No regression in rendering diff --git a/backlog/completed/ml-25 - recompact_positions-issues-per-row-UPDATEs-instead-of-bulk.md b/backlog/completed/ml-25 - recompact_positions-issues-per-row-UPDATEs-instead-of-bulk.md index 0e260e73..f78f4f43 100644 --- a/backlog/completed/ml-25 - recompact_positions-issues-per-row-UPDATEs-instead-of-bulk.md +++ b/backlog/completed/ml-25 - recompact_positions-issues-per-row-UPDATEs-instead-of-bulk.md @@ -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 + _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 + + - Position recompaction uses a single bulk UPDATE - Positions are correctly reassigned with no gaps - No regression in ordering behaviour diff --git a/backlog/completed/ml-29 - Collection-Wishlist-index-LiveView-duplication.md b/backlog/completed/ml-29 - Collection-Wishlist-index-LiveView-duplication.md index 0c61e055..a4579dba 100644 --- a/backlog/completed/ml-29 - Collection-Wishlist-index-LiveView-duplication.md +++ b/backlog/completed/ml-29 - Collection-Wishlist-index-LiveView-duplication.md @@ -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 + _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 + + - 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 diff --git a/backlog/completed/ml-3 - Add-tests-for-ArtistLive.Form-and-RecordSetLive.RecordPicker-LiveComponents.md b/backlog/completed/ml-3 - Add-tests-for-ArtistLive.Form-and-RecordSetLive.RecordPicker-LiveComponents.md index 07ee8e55..6fd55fea 100644 --- a/backlog/completed/ml-3 - Add-tests-for-ArtistLive.Form-and-RecordSetLive.RecordPicker-LiveComponents.md +++ b/backlog/completed/ml-3 - Add-tests-for-ArtistLive.Form-and-RecordSetLive.RecordPicker-LiveComponents.md @@ -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 + _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 + + - 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 diff --git a/backlog/completed/ml-30 - Silent-error-suppression-in-Artists.refresh_lastfm_data-1.md b/backlog/completed/ml-30 - Silent-error-suppression-in-Artists.refresh_lastfm_data-1.md index aea02586..6fc92908 100644 --- a/backlog/completed/ml-30 - Silent-error-suppression-in-Artists.refresh_lastfm_data-1.md +++ b/backlog/completed/ml-30 - Silent-error-suppression-in-Artists.refresh_lastfm_data-1.md @@ -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 + _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 + + - 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 diff --git a/backlog/completed/ml-31 - Deeply-nested-subqueries-in-ListeningStats.md b/backlog/completed/ml-31 - Deeply-nested-subqueries-in-ListeningStats.md index 3e5e8b1c..4dca7603 100644 --- a/backlog/completed/ml-31 - Deeply-nested-subqueries-in-ListeningStats.md +++ b/backlog/completed/ml-31 - Deeply-nested-subqueries-in-ListeningStats.md @@ -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 + _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 + + - Query plans for `list_tracks` and `recent_activity` show reduced nesting - No regression in query results - Benchmarks show comparable or better performance diff --git a/backlog/completed/ml-32 - OpenAI-API-uses-unsafe-bang-methods-and-lacks-rate-limiting.md b/backlog/completed/ml-32 - OpenAI-API-uses-unsafe-bang-methods-and-lacks-rate-limiting.md index 39db6ac2..d8327a57 100644 --- a/backlog/completed/ml-32 - OpenAI-API-uses-unsafe-bang-methods-and-lacks-rate-limiting.md +++ b/backlog/completed/ml-32 - OpenAI-API-uses-unsafe-bang-methods-and-lacks-rate-limiting.md @@ -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 + _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 + + - 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 diff --git a/backlog/completed/ml-33 - Align-Last.fm-callback-errors-with-app-conventions.md b/backlog/completed/ml-33 - Align-Last.fm-callback-errors-with-app-conventions.md index 5da6b83f..4d8c95d1 100644 --- a/backlog/completed/ml-33 - Align-Last.fm-callback-errors-with-app-conventions.md +++ b/backlog/completed/ml-33 - Align-Last.fm-callback-errors-with-app-conventions.md @@ -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 + _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 + + - 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. diff --git a/backlog/completed/ml-34 - Make-record-creation-resilient-to-color-extraction-failure.md b/backlog/completed/ml-34 - Make-record-creation-resilient-to-color-extraction-failure.md index 0dc0a84e..af260809 100644 --- a/backlog/completed/ml-34 - Make-record-creation-resilient-to-color-extraction-failure.md +++ b/backlog/completed/ml-34 - Make-record-creation-resilient-to-color-extraction-failure.md @@ -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 + _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 + + - `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. diff --git a/backlog/completed/ml-35 - Harden-the-public-asset-endpoint-against-invalid-payloads.md b/backlog/completed/ml-35 - Harden-the-public-asset-endpoint-against-invalid-payloads.md index 82df9ccc..0b9d17e9 100644 --- a/backlog/completed/ml-35 - Harden-the-public-asset-endpoint-against-invalid-payloads.md +++ b/backlog/completed/ml-35 - Harden-the-public-asset-endpoint-against-invalid-payloads.md @@ -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 + _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 + + - 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. diff --git a/backlog/completed/ml-36 - Return-controlled-errors-from-genre-population.md b/backlog/completed/ml-36 - Return-controlled-errors-from-genre-population.md index a0e80912..8fed0705 100644 --- a/backlog/completed/ml-36 - Return-controlled-errors-from-genre-population.md +++ b/backlog/completed/ml-36 - Return-controlled-errors-from-genre-population.md @@ -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 + _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 + + - 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. diff --git a/backlog/completed/ml-37 - Fix-production-migration-strategy-mismatch.md b/backlog/completed/ml-37 - Fix-production-migration-strategy-mismatch.md index 0d1645cf..35a36547 100644 --- a/backlog/completed/ml-37 - Fix-production-migration-strategy-mismatch.md +++ b/backlog/completed/ml-37 - Fix-production-migration-strategy-mismatch.md @@ -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 + _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 + + - 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. diff --git a/backlog/completed/ml-38 - Inconsistent-Logger-style-in-workers-eager-vs-lazy.md b/backlog/completed/ml-38 - Inconsistent-Logger-style-in-workers-eager-vs-lazy.md index 42fc7b94..8c109eab 100644 --- a/backlog/completed/ml-38 - Inconsistent-Logger-style-in-workers-eager-vs-lazy.md +++ b/backlog/completed/ml-38 - Inconsistent-Logger-style-in-workers-eager-vs-lazy.md @@ -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 + _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) + diff --git a/backlog/completed/ml-39 - Missing-dark-mode-class-in-search_components.ex.md b/backlog/completed/ml-39 - Missing-dark-mode-class-in-search_components.ex.md index 043657a9..0f6f8622 100644 --- a/backlog/completed/ml-39 - Missing-dark-mode-class-in-search_components.ex.md +++ b/backlog/completed/ml-39 - Missing-dark-mode-class-in-search_components.ex.md @@ -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 + _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) + diff --git a/backlog/completed/ml-4 - Improve-test-coverage-for-LiveHelpers.RecordActions-and-Components.Chat.md b/backlog/completed/ml-4 - Improve-test-coverage-for-LiveHelpers.RecordActions-and-Components.Chat.md index c95c0f1c..4c0bf9bf 100644 --- a/backlog/completed/ml-4 - Improve-test-coverage-for-LiveHelpers.RecordActions-and-Components.Chat.md +++ b/backlog/completed/ml-4 - Improve-test-coverage-for-LiveHelpers.RecordActions-and-Components.Chat.md @@ -3,17 +3,18 @@ id: ML-4 title: Improve test coverage for LiveHelpers.RecordActions and Components.Chat 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/179' + - "https://github.com/cloud8421/music_library/issues/179" priority: medium --- ## Description + _GitHub: created 2026-04-16 · updated 2026-04-17 · closed 2026-04-17_ ## Summary @@ -40,7 +41,9 @@ Both `LiveHelpers.RecordActions` and `Components.Chat` contain branches for erro - For `Components.Chat`: test the streaming state transitions (starting, receiving chunks, error, completed) and the message submission path. Streaming can be faked via `Req.Test` or behaviour stubs. ## Acceptance Criteria + + - `LiveHelpers.RecordActions` reaches at least 70 % line coverage - `Components.Chat` reaches at least 60 % line coverage - Both happy and error paths covered diff --git a/backlog/completed/ml-40 - Two-different-pagination-query-styles-across-contexts.md b/backlog/completed/ml-40 - Two-different-pagination-query-styles-across-contexts.md index 28b3d104..c52e62a8 100644 --- a/backlog/completed/ml-40 - Two-different-pagination-query-styles-across-contexts.md +++ b/backlog/completed/ml-40 - Two-different-pagination-query-styles-across-contexts.md @@ -3,17 +3,18 @@ id: ML-40 title: Two different pagination query styles across contexts 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/137' + - "https://github.com/cloud8421/music_library/issues/137" priority: low --- ## Description + _GitHub: created 2026-03-25 · updated 2026-03-25 · closed 2026-03-25_ ## Description @@ -21,12 +22,14 @@ _GitHub: created 2026-03-25 · updated 2026-03-25 · closed 2026-03-25_ Contexts use two different patterns for handling offset/limit pagination: **Style 1 — Keyword.get with defaults** (used by `Collection`, `RecordSets`): + ```elixir offset = Keyword.get(opts, :offset, 0) limit = Keyword.get(opts, :limit, @pagination[:default_page_size]) ``` **Style 2 — Case statements** (used by `ScrobbleRules`, `OnlineStoreTemplates`): + ```elixir case opts[:offset] do nil -> ... @@ -41,4 +44,5 @@ Standardize on one approach. The `Keyword.get` pattern is simpler and more commo ## Found during Codebase consistency audit (2026-03-25) + diff --git a/backlog/completed/ml-41 - ArtistInfo-changeset-uniquely-casts-id-field.md b/backlog/completed/ml-41 - ArtistInfo-changeset-uniquely-casts-id-field.md index 89354267..75c31272 100644 --- a/backlog/completed/ml-41 - ArtistInfo-changeset-uniquely-casts-id-field.md +++ b/backlog/completed/ml-41 - ArtistInfo-changeset-uniquely-casts-id-field.md @@ -1,19 +1,20 @@ --- id: ML-41 -title: 'ArtistInfo changeset uniquely casts :id field' +title: "ArtistInfo changeset uniquely casts :id field" 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/136' + - "https://github.com/cloud8421/music_library/issues/136" priority: low --- ## Description + _GitHub: created 2026-03-25 · updated 2026-03-25 · closed 2026-03-25_ ## Description @@ -25,4 +26,5 @@ This is likely intentional since artist IDs come from MusicBrainz, but the patte ## Found during Codebase consistency audit (2026-03-25) + diff --git a/backlog/completed/ml-42 - Missing-foreign_key_type-on-Note-and-ArtistInfo-schemas.md b/backlog/completed/ml-42 - Missing-foreign_key_type-on-Note-and-ArtistInfo-schemas.md index 27b62491..e1d7ef63 100644 --- a/backlog/completed/ml-42 - Missing-foreign_key_type-on-Note-and-ArtistInfo-schemas.md +++ b/backlog/completed/ml-42 - Missing-foreign_key_type-on-Note-and-ArtistInfo-schemas.md @@ -3,17 +3,18 @@ id: ML-42 title: Missing @foreign_key_type on Note and ArtistInfo schemas 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/135' + - "https://github.com/cloud8421/music_library/issues/135" priority: low --- ## Description + _GitHub: created 2026-03-25 · updated 2026-03-25 · closed 2026-03-25_ ## Description @@ -32,4 +33,5 @@ Add `@foreign_key_type :binary_id` after the `@primary_key` declaration in both ## Found during Codebase consistency audit (2026-03-25) + diff --git a/backlog/completed/ml-43 - Wikipedia.Config-missing-enforce_keys.md b/backlog/completed/ml-43 - Wikipedia.Config-missing-enforce_keys.md index 61fded8d..ad7089cc 100644 --- a/backlog/completed/ml-43 - Wikipedia.Config-missing-enforce_keys.md +++ b/backlog/completed/ml-43 - Wikipedia.Config-missing-enforce_keys.md @@ -3,17 +3,18 @@ id: ML-43 title: Wikipedia.Config missing @enforce_keys 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/134' + - "https://github.com/cloud8421/music_library/issues/134" priority: medium --- ## Description + _GitHub: created 2026-03-25 · updated 2026-03-25 · closed 2026-03-25_ ## Description @@ -27,4 +28,5 @@ Add `@enforce_keys [:user_agent]` (or whichever fields are required) to `Wikiped ## Found during Codebase consistency audit (2026-03-25) + diff --git a/backlog/completed/ml-44 - Wikipedia-and-BraveSearch-APIs-missing-rate-limiting.md b/backlog/completed/ml-44 - Wikipedia-and-BraveSearch-APIs-missing-rate-limiting.md index f2159155..760654a4 100644 --- a/backlog/completed/ml-44 - Wikipedia-and-BraveSearch-APIs-missing-rate-limiting.md +++ b/backlog/completed/ml-44 - Wikipedia-and-BraveSearch-APIs-missing-rate-limiting.md @@ -3,17 +3,18 @@ id: ML-44 title: Wikipedia and BraveSearch APIs missing rate limiting 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/133' + - "https://github.com/cloud8421/music_library/issues/133" priority: medium --- ## Description + _GitHub: created 2026-03-25 · updated 2026-03-25 · closed 2026-03-25_ ## Description @@ -29,4 +30,5 @@ Attach `Req.RateLimiter` with appropriate cooldown values to both Wikipedia.API ## Found during Codebase consistency audit (2026-03-25) + diff --git a/backlog/completed/ml-45 - Inconsistent-fetch_-vs-refresh_-naming-in-Artists-context.md b/backlog/completed/ml-45 - Inconsistent-fetch_-vs-refresh_-naming-in-Artists-context.md index 47b8782d..e904be33 100644 --- a/backlog/completed/ml-45 - Inconsistent-fetch_-vs-refresh_-naming-in-Artists-context.md +++ b/backlog/completed/ml-45 - Inconsistent-fetch_-vs-refresh_-naming-in-Artists-context.md @@ -3,17 +3,18 @@ id: ML-45 title: Inconsistent fetch_* vs refresh_* naming in Artists context 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/132' + - "https://github.com/cloud8421/music_library/issues/132" priority: medium --- ## Description + _GitHub: created 2026-03-25 · updated 2026-03-25 · closed 2026-03-25_ ## Description @@ -35,4 +36,5 @@ Standardize on one naming convention. Since `Records` context uses only `refresh ## Found during Codebase consistency audit (2026-03-25) + diff --git a/backlog/completed/ml-46 - FetchArtistInfo-worker-contains-business-logic.md b/backlog/completed/ml-46 - FetchArtistInfo-worker-contains-business-logic.md index f1d24ade..927e5125 100644 --- a/backlog/completed/ml-46 - FetchArtistInfo-worker-contains-business-logic.md +++ b/backlog/completed/ml-46 - FetchArtistInfo-worker-contains-business-logic.md @@ -3,17 +3,18 @@ id: ML-46 title: FetchArtistInfo worker contains business logic 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/131' + - "https://github.com/cloud8421/music_library/issues/131" priority: medium --- ## Description + _GitHub: created 2026-03-25 · updated 2026-03-25 · closed 2026-03-25_ ## Description @@ -27,4 +28,5 @@ Extract `regenerate_record_embeddings/1` into a context function (e.g. `Records. ## Found during Codebase consistency audit (2026-03-25) + diff --git a/backlog/completed/ml-47 - PruneAssets-worker-has-direct-Repo-queries.md b/backlog/completed/ml-47 - PruneAssets-worker-has-direct-Repo-queries.md index fad3a84a..2ba27991 100644 --- a/backlog/completed/ml-47 - PruneAssets-worker-has-direct-Repo-queries.md +++ b/backlog/completed/ml-47 - PruneAssets-worker-has-direct-Repo-queries.md @@ -3,17 +3,18 @@ id: ML-47 title: PruneAssets worker has direct Repo queries 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/130' + - "https://github.com/cloud8421/music_library/issues/130" priority: medium --- ## Description + _GitHub: created 2026-03-25 · updated 2026-03-25 · closed 2026-03-25_ ## Description @@ -27,4 +28,5 @@ Extract the query and deletion into an `Assets.prune_unreferenced/0` context fun ## Found during Codebase consistency audit (2026-03-25) + diff --git a/backlog/completed/ml-48 - ScrobbleActivity-context-has-zero-test-coverage.md b/backlog/completed/ml-48 - ScrobbleActivity-context-has-zero-test-coverage.md index 1d045cf4..8fc1a0ae 100644 --- a/backlog/completed/ml-48 - ScrobbleActivity-context-has-zero-test-coverage.md +++ b/backlog/completed/ml-48 - ScrobbleActivity-context-has-zero-test-coverage.md @@ -3,17 +3,18 @@ id: ML-48 title: ScrobbleActivity context has zero test coverage 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/127' + - "https://github.com/cloud8421/music_library/issues/127" priority: medium --- ## Description + _GitHub: created 2026-03-22 · updated 2026-03-23 · closed 2026-03-23_ ## Description diff --git a/backlog/completed/ml-49 - BarcodeScan-context-has-zero-test-coverage.md b/backlog/completed/ml-49 - BarcodeScan-context-has-zero-test-coverage.md index 1ceaf7db..b4bb3833 100644 --- a/backlog/completed/ml-49 - BarcodeScan-context-has-zero-test-coverage.md +++ b/backlog/completed/ml-49 - BarcodeScan-context-has-zero-test-coverage.md @@ -3,17 +3,18 @@ id: ML-49 title: BarcodeScan context has zero test coverage 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/126' + - "https://github.com/cloud8421/music_library/issues/126" priority: medium --- ## Description + _GitHub: created 2026-03-22 · updated 2026-03-22 · closed 2026-03-22_ ## Description diff --git a/backlog/completed/ml-5 - Document-Last.fm-OAuth-callback-trust-boundary.md b/backlog/completed/ml-5 - Document-Last.fm-OAuth-callback-trust-boundary.md index 6a7fc9f8..aa1ace33 100644 --- a/backlog/completed/ml-5 - Document-Last.fm-OAuth-callback-trust-boundary.md +++ b/backlog/completed/ml-5 - Document-Last.fm-OAuth-callback-trust-boundary.md @@ -4,18 +4,19 @@ title: Document Last.fm OAuth callback trust boundary status: Done assignee: - Claudio Ortolina -created_date: '2026-04-20 08:48' -updated_date: '2026-04-24 07:04' +created_date: "2026-04-20 08:48" +updated_date: "2026-04-24 07:04" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/178' + - "https://github.com/cloud8421/music_library/issues/178" priority: low --- ## Description + _GitHub: created 2026-04-16 · updated 2026-04-16_ ## Summary @@ -38,10 +39,13 @@ Add a short `@moduledoc` or inline comment in `last_fm_controller.ex` explaining 3. What the failure modes look like (invalid token → store nothing, error logged) Similarly in `router.ex:55`, a brief comment noting the deliberate exception from `:logged_in`. + ## Acceptance Criteria + + - [x] #1 last_fm_controller.ex has a comment documenting the trust boundary - [x] #2 router.ex:55 notes the deliberate pipeline exemption @@ -49,6 +53,7 @@ Similarly in `router.ex:55`, a brief comment noting the deliberate exception fro ## Implementation Plan + ## Implementation Plan 1. **`lib/music_library_web/controllers/last_fm_controller.ex`** — add `@moduledoc` covering: @@ -69,6 +74,7 @@ Similarly in `router.ex:55`, a brief comment noting the deliberate exception fro ## Final Summary + ## Summary Documented the Last.fm OAuth callback trust boundary in two places: @@ -83,4 +89,5 @@ Documented the Last.fm OAuth callback trust boundary in two places: ## Notes Controllers are excluded from the `Credo.Check.Readability.ModuleDoc` regex in `.credo.exs`, so the `@moduledoc` here is a deliberate choice for a security-sensitive module, not a lint requirement. + diff --git a/backlog/completed/ml-50 - Duplicated-parse-helpers-in-Collection-Wishlist-Index.md b/backlog/completed/ml-50 - Duplicated-parse-helpers-in-Collection-Wishlist-Index.md index 13afdbd6..4c646d49 100644 --- a/backlog/completed/ml-50 - Duplicated-parse-helpers-in-Collection-Wishlist-Index.md +++ b/backlog/completed/ml-50 - Duplicated-parse-helpers-in-Collection-Wishlist-Index.md @@ -3,17 +3,18 @@ id: ML-50 title: Duplicated parse helpers in Collection/Wishlist Index 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/125' + - "https://github.com/cloud8421/music_library/issues/125" priority: medium --- ## Description + _GitHub: created 2026-03-22 · updated 2026-03-22 · closed 2026-03-22_ ## Description @@ -28,4 +29,5 @@ _GitHub: created 2026-03-22 · updated 2026-03-22 · closed 2026-03-22_ ## Suggested approach Extract into a shared helper module (e.g. `MusicLibraryWeb.LiveHelpers.Params` which already exists for pagination). + diff --git a/backlog/completed/ml-51 - ArtistLive.Show-is-823-lines.md b/backlog/completed/ml-51 - ArtistLive.Show-is-823-lines.md index 27379958..25292be6 100644 --- a/backlog/completed/ml-51 - ArtistLive.Show-is-823-lines.md +++ b/backlog/completed/ml-51 - ArtistLive.Show-is-823-lines.md @@ -3,17 +3,18 @@ id: ML-51 title: ArtistLive.Show is 823 lines 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/124' + - "https://github.com/cloud8421/music_library/issues/124" priority: medium --- ## Description + _GitHub: created 2026-03-22 · updated 2026-03-22 · closed 2026-03-22_ ## Description diff --git a/backlog/completed/ml-52 - Collection-Wishlist-Show-LiveViews-duplication.md b/backlog/completed/ml-52 - Collection-Wishlist-Show-LiveViews-duplication.md index bfbcbaa1..79a712f6 100644 --- a/backlog/completed/ml-52 - Collection-Wishlist-Show-LiveViews-duplication.md +++ b/backlog/completed/ml-52 - Collection-Wishlist-Show-LiveViews-duplication.md @@ -3,17 +3,18 @@ id: ML-52 title: Collection/Wishlist Show LiveViews duplication 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/123' + - "https://github.com/cloud8421/music_library/issues/123" priority: medium --- ## Description + _GitHub: created 2026-03-22 · updated 2026-03-22 · closed 2026-03-22_ ## Description @@ -28,4 +29,5 @@ _GitHub: created 2026-03-22 · updated 2026-03-22 · closed 2026-03-22_ ## Suggested approach Extract shared record action handlers into a helper module that both LiveViews can delegate to. + diff --git a/backlog/completed/ml-53 - Rewrite-vertical-bar-chart-component.md b/backlog/completed/ml-53 - Rewrite-vertical-bar-chart-component.md index e284631c..bb23cc30 100644 --- a/backlog/completed/ml-53 - Rewrite-vertical-bar-chart-component.md +++ b/backlog/completed/ml-53 - Rewrite-vertical-bar-chart-component.md @@ -3,20 +3,22 @@ id: ML-53 title: Rewrite vertical bar chart component status: Done assignee: [] -created_date: '2026-04-20 08:54' +created_date: "2026-04-20 08:54" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/122' + - "https://github.com/cloud8421/music_library/issues/122" priority: high --- ## Description + _GitHub: created 2026-03-21 · updated 2026-03-21 · closed 2026-03-21_ The current vertical chart component uses SVG, which means it's not natively responsive, and it has to rely on approximated calculations which make it difficult to render nicely at every viewport. It needs to be rewritten with standard html elements laid out in a responsive grid, so that it renders appropriately for every viewport. + diff --git a/backlog/completed/ml-54 - Standardize-play-icon-sizing.md b/backlog/completed/ml-54 - Standardize-play-icon-sizing.md index a100d1d7..a5079b7b 100644 --- a/backlog/completed/ml-54 - Standardize-play-icon-sizing.md +++ b/backlog/completed/ml-54 - Standardize-play-icon-sizing.md @@ -3,17 +3,18 @@ id: ML-54 title: Standardize play icon sizing status: Done assignee: [] -created_date: '2026-04-20 08:54' +created_date: "2026-04-20 08:54" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/121' + - "https://github.com/cloud8421/music_library/issues/121" priority: medium --- ## Description + _GitHub: created 2026-03-16 · updated 2026-03-16 · closed 2026-03-16_ ## Problem @@ -27,4 +28,5 @@ The play/scrobble icon (`hero-play`) uses three different sizing approaches: ## Suggestion The button group context (`h-5 w-5`) vs dropdown/inline context (`h-4 w-4`) might be intentional. If so, document the sizing convention. The `class="icon"` usage should align with one of these. + diff --git a/backlog/completed/ml-55 - Standardize-stats-dashboard-gap-sizes.md b/backlog/completed/ml-55 - Standardize-stats-dashboard-gap-sizes.md index 5b3dbe1a..24eeff8f 100644 --- a/backlog/completed/ml-55 - Standardize-stats-dashboard-gap-sizes.md +++ b/backlog/completed/ml-55 - Standardize-stats-dashboard-gap-sizes.md @@ -3,17 +3,18 @@ id: ML-55 title: Standardize stats dashboard gap sizes status: Done assignee: [] -created_date: '2026-04-20 08:54' +created_date: "2026-04-20 08:54" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/120' + - "https://github.com/cloud8421/music_library/issues/120" priority: medium --- ## Description + _GitHub: created 2026-03-16 · updated 2026-03-16 · closed 2026-03-16_ ## Problem @@ -28,4 +29,5 @@ Within the stats dashboard (`stats_live/index.ex`), multiple gap sizes are used ## Suggestion Use a single gap value (e.g. `gap-5`) across all stats grid sections for visual consistency. + diff --git a/backlog/completed/ml-56 - Standardize-card-border-radius-and-shadow.md b/backlog/completed/ml-56 - Standardize-card-border-radius-and-shadow.md index c08835fe..ec97642d 100644 --- a/backlog/completed/ml-56 - Standardize-card-border-radius-and-shadow.md +++ b/backlog/completed/ml-56 - Standardize-card-border-radius-and-shadow.md @@ -3,17 +3,18 @@ id: ML-56 title: Standardize card border-radius and shadow status: Done assignee: [] -created_date: '2026-04-20 08:54' +created_date: "2026-04-20 08:54" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/119' + - "https://github.com/cloud8421/music_library/issues/119" priority: medium --- ## Description + _GitHub: created 2026-03-16 · updated 2026-03-16 · closed 2026-03-16_ ## Problem @@ -26,4 +27,5 @@ Card/panel components use inconsistent rounding and shadows: ## Suggestion Pick one card style (e.g. `rounded-lg` without shadow, or `rounded-md` with `shadow-sm`) and apply consistently. + diff --git a/backlog/completed/ml-57 - Standardize-grid-gap-values-across-record-grids.md b/backlog/completed/ml-57 - Standardize-grid-gap-values-across-record-grids.md index 39310b85..ec1e8531 100644 --- a/backlog/completed/ml-57 - Standardize-grid-gap-values-across-record-grids.md +++ b/backlog/completed/ml-57 - Standardize-grid-gap-values-across-record-grids.md @@ -3,17 +3,18 @@ id: ML-57 title: Standardize grid gap values across record grids status: Done assignee: [] -created_date: '2026-04-20 08:54' +created_date: "2026-04-20 08:54" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/118' + - "https://github.com/cloud8421/music_library/issues/118" priority: medium --- ## Description + _GitHub: created 2026-03-16 · updated 2026-03-16 · closed 2026-03-16_ ## Problem @@ -27,4 +28,5 @@ Different record grid contexts use different vertical gap values: ## Decision needed Should all record grids share a consistent `gap-y` value, or are these intentional density differences for different contexts? + diff --git a/backlog/completed/ml-58 - Standardize-page-header-top-margin.md b/backlog/completed/ml-58 - Standardize-page-header-top-margin.md index 1fe68227..0bacbaf3 100644 --- a/backlog/completed/ml-58 - Standardize-page-header-top-margin.md +++ b/backlog/completed/ml-58 - Standardize-page-header-top-margin.md @@ -3,17 +3,18 @@ id: ML-58 title: Standardize page header top margin status: Done assignee: [] -created_date: '2026-04-20 08:54' +created_date: "2026-04-20 08:54" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/117' + - "https://github.com/cloud8421/music_library/issues/117" priority: medium --- ## Description + _GitHub: created 2026-03-16 · updated 2026-03-16 · closed 2026-03-16_ ## Problem @@ -27,4 +28,5 @@ Page-level section headers use inconsistent top margins: ## Suggestion Pick a standard margin (e.g. `mt-5`) for all page-level h1 section headers. + diff --git a/backlog/completed/ml-59 - Standardize-list-separation-pattern-divide-y-vs-space-y.md b/backlog/completed/ml-59 - Standardize-list-separation-pattern-divide-y-vs-space-y.md index 4f30d4e6..59de1bcb 100644 --- a/backlog/completed/ml-59 - Standardize-list-separation-pattern-divide-y-vs-space-y.md +++ b/backlog/completed/ml-59 - Standardize-list-separation-pattern-divide-y-vs-space-y.md @@ -3,17 +3,18 @@ id: ML-59 title: Standardize list separation pattern (divide-y vs space-y) status: Done assignee: [] -created_date: '2026-04-20 08:54' +created_date: "2026-04-20 08:54" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/116' + - "https://github.com/cloud8421/music_library/issues/116" priority: medium --- ## Description + _GitHub: created 2026-03-16 · updated 2026-03-16 · closed 2026-03-16_ ## Problem @@ -26,4 +27,5 @@ Lists use two different separation approaches: ## Decision needed Should all lists use `divide-y` for consistency, or is `space-y` acceptable for card-style list items where dividers would feel heavy? + diff --git a/backlog/completed/ml-6 - Align-mix.exs-test-coverage-threshold-with-sandbox-achievable-level.md b/backlog/completed/ml-6 - Align-mix.exs-test-coverage-threshold-with-sandbox-achievable-level.md index b746408e..a43ff782 100644 --- a/backlog/completed/ml-6 - Align-mix.exs-test-coverage-threshold-with-sandbox-achievable-level.md +++ b/backlog/completed/ml-6 - Align-mix.exs-test-coverage-threshold-with-sandbox-achievable-level.md @@ -3,17 +3,18 @@ id: ML-6 title: Align mix.exs test coverage threshold with sandbox-achievable level 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/177' + - "https://github.com/cloud8421/music_library/issues/177" priority: low --- ## Description + _GitHub: created 2026-04-16 · updated 2026-04-17 · closed 2026-04-17_ ## Summary @@ -25,6 +26,7 @@ _GitHub: created 2026-04-16 · updated 2026-04-17 · closed 2026-04-17_ Current coverage output: 75.77 % (720 tests, `mix test --cover`). 0 % modules that cannot be covered under the sandbox convention: + - `MusicLibrary.Worker.RepoVacuum` - `MusicLibrary.Worker.RepoOptimize` - `MusicLibrary.Worker.SendRecordsOnThisDayEmail` @@ -44,7 +46,9 @@ A red coverage check on `mix test --cover` that is structurally unachievable tra Option 2 is preferable because it keeps the threshold aspirational and surfaces the exclusion list. ## Acceptance Criteria + + - `mix test --cover` passes under its configured threshold - Untestable modules are explicitly enumerated somewhere (config, moduledoc, or both) diff --git a/backlog/completed/ml-60 - Unify-genre-tag-styling-between-edit-and-display.md b/backlog/completed/ml-60 - Unify-genre-tag-styling-between-edit-and-display.md index 18ff7d90..9f2e7ae1 100644 --- a/backlog/completed/ml-60 - Unify-genre-tag-styling-between-edit-and-display.md +++ b/backlog/completed/ml-60 - Unify-genre-tag-styling-between-edit-and-display.md @@ -3,17 +3,18 @@ id: ML-60 title: Unify genre tag styling between edit and display status: Done assignee: [] -created_date: '2026-04-20 08:54' +created_date: "2026-04-20 08:54" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/115' + - "https://github.com/cloud8421/music_library/issues/115" priority: medium --- ## Description + _GitHub: created 2026-03-16 · updated 2026-03-16 · closed 2026-03-16_ ## Problem @@ -26,4 +27,5 @@ Genre tags render differently in edit vs read contexts: ## Decision needed Should the edit form use `.badge variant="soft"` with an appended remove button to match the read view? Or is the visual difference acceptable since the edit form needs the remove interaction? + diff --git a/backlog/completed/ml-61 - Standardize-dropdown-toggle-variant-across-pages.md b/backlog/completed/ml-61 - Standardize-dropdown-toggle-variant-across-pages.md index 4f5aedf0..c0bc721f 100644 --- a/backlog/completed/ml-61 - Standardize-dropdown-toggle-variant-across-pages.md +++ b/backlog/completed/ml-61 - Standardize-dropdown-toggle-variant-across-pages.md @@ -3,17 +3,18 @@ id: ML-61 title: Standardize dropdown toggle variant across pages status: Done assignee: [] -created_date: '2026-04-20 08:54' +created_date: "2026-04-20 08:54" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/114' + - "https://github.com/cloud8421/music_library/issues/114" priority: medium --- ## Description + _GitHub: created 2026-03-16 · updated 2026-03-16 · closed 2026-03-16_ ## Problem @@ -27,4 +28,5 @@ Show pages use `variant="soft"` for dropdown toggle buttons while index pages us ## Decision needed Pick one variant for all action dropdown toggles, or document the show/index distinction as intentional. + diff --git a/backlog/completed/ml-62 - Route-Search-artist-queries-through-Artists-context.md b/backlog/completed/ml-62 - Route-Search-artist-queries-through-Artists-context.md index 3e6ccda5..7c077d3f 100644 --- a/backlog/completed/ml-62 - Route-Search-artist-queries-through-Artists-context.md +++ b/backlog/completed/ml-62 - Route-Search-artist-queries-through-Artists-context.md @@ -3,17 +3,18 @@ id: ML-62 title: Route Search artist queries through Artists context status: Done assignee: [] -created_date: '2026-04-20 08:54' +created_date: "2026-04-20 08:54" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/113' + - "https://github.com/cloud8421/music_library/issues/113" priority: low --- ## Description + _GitHub: created 2026-03-12 · updated 2026-03-12 · closed 2026-03-12_ ## Problem diff --git a/backlog/completed/ml-63 - Break-bidirectional-dependency-between-Records-and-Artists.md b/backlog/completed/ml-63 - Break-bidirectional-dependency-between-Records-and-Artists.md index eb390e95..af5e1c5a 100644 --- a/backlog/completed/ml-63 - Break-bidirectional-dependency-between-Records-and-Artists.md +++ b/backlog/completed/ml-63 - Break-bidirectional-dependency-between-Records-and-Artists.md @@ -3,17 +3,18 @@ id: ML-63 title: Break bidirectional dependency between Records and Artists status: Done assignee: [] -created_date: '2026-04-20 08:54' +created_date: "2026-04-20 08:54" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/112' + - "https://github.com/cloud8421/music_library/issues/112" priority: medium --- ## Description + _GitHub: created 2026-03-12 · updated 2026-03-12 · closed 2026-03-12_ ## Problem diff --git a/backlog/completed/ml-64 - Split-ScrobbleActivity-into-focused-modules.md b/backlog/completed/ml-64 - Split-ScrobbleActivity-into-focused-modules.md index 0e9d6d2c..b07aac72 100644 --- a/backlog/completed/ml-64 - Split-ScrobbleActivity-into-focused-modules.md +++ b/backlog/completed/ml-64 - Split-ScrobbleActivity-into-focused-modules.md @@ -3,17 +3,18 @@ id: ML-64 title: Split ScrobbleActivity into focused modules status: Done assignee: [] -created_date: '2026-04-20 08:54' +created_date: "2026-04-20 08:54" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/111' + - "https://github.com/cloud8421/music_library/issues/111" priority: low --- ## Description + _GitHub: created 2026-03-12 · updated 2026-03-12 · closed 2026-03-12_ ## Problem diff --git a/backlog/completed/ml-65 - Move-scrobble-queries-out-of-Records-context.md b/backlog/completed/ml-65 - Move-scrobble-queries-out-of-Records-context.md index 444c8814..9f75c83a 100644 --- a/backlog/completed/ml-65 - Move-scrobble-queries-out-of-Records-context.md +++ b/backlog/completed/ml-65 - Move-scrobble-queries-out-of-Records-context.md @@ -3,17 +3,18 @@ id: ML-65 title: Move scrobble queries out of Records context status: Done 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/110' + - "https://github.com/cloud8421/music_library/issues/110" priority: medium --- ## Description + _GitHub: created 2026-03-12 · updated 2026-03-12 · closed 2026-03-12_ ## Problem diff --git a/backlog/completed/ml-67 - Credo-ModuleDoc-check-still-disabled.md b/backlog/completed/ml-67 - Credo-ModuleDoc-check-still-disabled.md index 28a0ac39..68140bce 100644 --- a/backlog/completed/ml-67 - Credo-ModuleDoc-check-still-disabled.md +++ b/backlog/completed/ml-67 - Credo-ModuleDoc-check-still-disabled.md @@ -3,17 +3,18 @@ id: ML-67 title: Credo ModuleDoc check still disabled status: Done 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/108' + - "https://github.com/cloud8421/music_library/issues/108" priority: low --- ## Description + _GitHub: created 2026-03-12 · updated 2026-03-13 · closed 2026-03-13_ ## Description @@ -27,4 +28,5 @@ Consider re-enabling `ModuleDoc` (possibly with exclusions for specific module p ## Source From technical debt audit (2026-03-12). Residual from #84. + diff --git a/backlog/completed/ml-7 - Tighten-vague-test-assertions-in-scrobble_activity-and-worker-tests.md b/backlog/completed/ml-7 - Tighten-vague-test-assertions-in-scrobble_activity-and-worker-tests.md index 8649e73a..3b9067b4 100644 --- a/backlog/completed/ml-7 - Tighten-vague-test-assertions-in-scrobble_activity-and-worker-tests.md +++ b/backlog/completed/ml-7 - Tighten-vague-test-assertions-in-scrobble_activity-and-worker-tests.md @@ -3,17 +3,18 @@ id: ML-7 title: Tighten vague test assertions in scrobble_activity and worker tests 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/176' + - "https://github.com/cloud8421/music_library/issues/176" priority: low --- ## Description + _GitHub: created 2026-04-16 · updated 2026-04-20 · closed 2026-04-20_ ## Summary @@ -46,7 +47,9 @@ The context's entire job is to shape Last.fm payloads correctly, yet the tests d For each site, replace with an assertion that pins a specific value the function is responsible for producing. ## Acceptance Criteria + + - `scrobble_activity_test.exs` tests assert specific struct fields on each `{:ok, _}` site - No `assert X != nil` in worker test files (replace with value assertion) - Suite still passes diff --git a/backlog/completed/ml-71 - page_title-2-public-in-two-LiveViews.md b/backlog/completed/ml-71 - page_title-2-public-in-two-LiveViews.md index cc06df70..1b63310e 100644 --- a/backlog/completed/ml-71 - page_title-2-public-in-two-LiveViews.md +++ b/backlog/completed/ml-71 - page_title-2-public-in-two-LiveViews.md @@ -3,17 +3,18 @@ id: ML-71 title: page_title/2 public in two LiveViews status: Done 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/104' + - "https://github.com/cloud8421/music_library/issues/104" priority: low --- ## Description + _GitHub: created 2026-03-12 · updated 2026-03-12 · closed 2026-03-12_ ## Description @@ -32,4 +33,5 @@ Change `def page_title` to `defp page_title` in both files. ## Source From technical debt audit (2026-03-12). + diff --git a/backlog/completed/ml-72 - TopAlbums-TopArtists-structural-duplication.md b/backlog/completed/ml-72 - TopAlbums-TopArtists-structural-duplication.md index d409a661..0cf48517 100644 --- a/backlog/completed/ml-72 - TopAlbums-TopArtists-structural-duplication.md +++ b/backlog/completed/ml-72 - TopAlbums-TopArtists-structural-duplication.md @@ -3,17 +3,18 @@ id: ML-72 title: TopAlbums/TopArtists structural duplication status: Done 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/103' + - "https://github.com/cloud8421/music_library/issues/103" priority: low --- ## Description + _GitHub: created 2026-03-12 · updated 2026-03-12 · closed 2026-03-12_ ## Description @@ -29,4 +30,5 @@ Consider extracting a parameterized `TopByPeriod` component that accepts data-fe ## Source From technical debt audit (2026-03-12). + diff --git a/backlog/completed/ml-73 - Sanitize-markdown-HTML-output-in-search-and-record-set-components.md b/backlog/completed/ml-73 - Sanitize-markdown-HTML-output-in-search-and-record-set-components.md index fdfbc66d..9ba7190f 100644 --- a/backlog/completed/ml-73 - Sanitize-markdown-HTML-output-in-search-and-record-set-components.md +++ b/backlog/completed/ml-73 - Sanitize-markdown-HTML-output-in-search-and-record-set-components.md @@ -3,17 +3,18 @@ id: ML-73 title: Sanitize markdown HTML output in search and record set components status: Done assignee: [] -created_date: '2026-04-20 08:56' +created_date: "2026-04-20 08:56" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/102' + - "https://github.com/cloud8421/music_library/issues/102" priority: low --- ## Description + _GitHub: created 2026-03-07 · updated 2026-03-07 · closed 2026-03-07_ The following locations render markdown descriptions via `Markdown.to_html()` + `raw()` without HTML sanitization: @@ -23,4 +24,5 @@ The following locations render markdown descriptions via `Markdown.to_html()` + - `lib/music_library_web/live/record_set_live/show.ex:308` Same pattern as the Notes component. Should sanitize for defense in depth. + diff --git a/backlog/completed/ml-74 - Sanitize-markdown-HTML-output-in-Notes-component.md b/backlog/completed/ml-74 - Sanitize-markdown-HTML-output-in-Notes-component.md index 4d2efa86..723915bc 100644 --- a/backlog/completed/ml-74 - Sanitize-markdown-HTML-output-in-Notes-component.md +++ b/backlog/completed/ml-74 - Sanitize-markdown-HTML-output-in-Notes-component.md @@ -3,20 +3,22 @@ id: ML-74 title: Sanitize markdown HTML output in Notes component status: Done assignee: [] -created_date: '2026-04-20 08:56' +created_date: "2026-04-20 08:56" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/101' + - "https://github.com/cloud8421/music_library/issues/101" priority: low --- ## Description + _GitHub: created 2026-03-07 · updated 2026-03-07 · closed 2026-03-07_ `lib/music_library_web/components/notes.ex:162` renders user-authored markdown via `Earmark.as_html!/2` + `raw()` without HTML sanitization. Currently single-user so self-XSS only, but should sanitize for defense in depth. + diff --git a/backlog/completed/ml-75 - Add-Content-Security-Policy-header-to-browser-pipeline.md b/backlog/completed/ml-75 - Add-Content-Security-Policy-header-to-browser-pipeline.md index c634b9c0..1864f09a 100644 --- a/backlog/completed/ml-75 - Add-Content-Security-Policy-header-to-browser-pipeline.md +++ b/backlog/completed/ml-75 - Add-Content-Security-Policy-header-to-browser-pipeline.md @@ -3,20 +3,22 @@ id: ML-75 title: Add Content-Security-Policy header to browser pipeline status: Done assignee: [] -created_date: '2026-04-20 08:56' +created_date: "2026-04-20 08:56" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/100' + - "https://github.com/cloud8421/music_library/issues/100" priority: low --- ## Description + _GitHub: created 2026-03-07 · updated 2026-03-07 · closed 2026-03-07_ Sobelow reports missing CSP on the browser pipeline (`router.ex:14`). Add a basic CSP via `put_secure_browser_headers/2`. Currently the app does not set a `Content-Security-Policy` header, which is a defense-in-depth measure against XSS and other injection attacks. + diff --git a/backlog/completed/ml-77 - Inconsistent-alphabetical-ordering-helpers.md b/backlog/completed/ml-77 - Inconsistent-alphabetical-ordering-helpers.md index 3c2039bd..b8850b57 100644 --- a/backlog/completed/ml-77 - Inconsistent-alphabetical-ordering-helpers.md +++ b/backlog/completed/ml-77 - Inconsistent-alphabetical-ordering-helpers.md @@ -3,17 +3,18 @@ id: ML-77 title: Inconsistent alphabetical ordering helpers status: Done 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/98' + - "https://github.com/cloud8421/music_library/issues/98" priority: low --- ## Description + _GitHub: created 2026-03-05 · updated 2026-03-06 · closed 2026-03-06_ ## Priority: Low @@ -36,4 +37,5 @@ Parameterize the `order_alphabetically` macro to accept a field and share it acr ## Source From technical debt audit (2026-03-05). + diff --git a/backlog/completed/ml-78 - Commented-out-code-in-error_json.ex.md b/backlog/completed/ml-78 - Commented-out-code-in-error_json.ex.md index 8e5aaccb..a6a3862a 100644 --- a/backlog/completed/ml-78 - Commented-out-code-in-error_json.ex.md +++ b/backlog/completed/ml-78 - Commented-out-code-in-error_json.ex.md @@ -3,17 +3,18 @@ id: ML-78 title: Commented-out code in error_json.ex status: Done 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/97' + - "https://github.com/cloud8421/music_library/issues/97" priority: low --- ## Description + _GitHub: created 2026-03-05 · updated 2026-03-06 · closed 2026-03-06_ ## Priority: Low @@ -29,4 +30,5 @@ Either remove the commented-out code or restore it if needed. ## Source From technical debt audit (2026-03-05). + diff --git a/backlog/completed/ml-79 - Chat-implementations-have-trivial-test-coverage.md b/backlog/completed/ml-79 - Chat-implementations-have-trivial-test-coverage.md index 9ceea0fc..3d739d47 100644 --- a/backlog/completed/ml-79 - Chat-implementations-have-trivial-test-coverage.md +++ b/backlog/completed/ml-79 - Chat-implementations-have-trivial-test-coverage.md @@ -3,17 +3,18 @@ id: ML-79 title: Chat implementations have trivial test coverage status: Done 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/96' + - "https://github.com/cloud8421/music_library/issues/96" priority: medium --- ## Description + _GitHub: created 2026-03-05 · updated 2026-03-05 · closed 2026-03-05_ ## Priority: Medium @@ -34,4 +35,5 @@ Add tests for instruction building and context assembly logic. ## Source From technical debt audit (2026-03-05). + diff --git a/backlog/completed/ml-8 - Add-docker-ecosystem-to-Dependabot-for-Dockerfile-base-images.md b/backlog/completed/ml-8 - Add-docker-ecosystem-to-Dependabot-for-Dockerfile-base-images.md index acd3ad37..dcd04563 100644 --- a/backlog/completed/ml-8 - Add-docker-ecosystem-to-Dependabot-for-Dockerfile-base-images.md +++ b/backlog/completed/ml-8 - Add-docker-ecosystem-to-Dependabot-for-Dockerfile-base-images.md @@ -3,17 +3,18 @@ id: ML-8 title: Add docker ecosystem to Dependabot for Dockerfile base images 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/175' + - "https://github.com/cloud8421/music_library/issues/175" priority: low --- ## Description + _GitHub: created 2026-04-16 · updated 2026-04-17 · closed 2026-04-17_ ## Summary @@ -40,7 +41,9 @@ Add to `.github/dependabot.yml`: Dependabot will open PRs bumping the base image ARGs; the `mise run dev:validate-docker-image` task (already in the project) will need to be run on those PRs to confirm `hexpm/elixir` tag availability on both `linux/amd64` and `linux/arm64`. ## Acceptance Criteria + + - Docker base-image updates surface as Dependabot PRs - Existing `validate-docker-image` task runs in CI to gate them diff --git a/backlog/completed/ml-80 - ScrobbleLive-has-zero-test-coverage.md b/backlog/completed/ml-80 - ScrobbleLive-has-zero-test-coverage.md index 38110aef..07d03de5 100644 --- a/backlog/completed/ml-80 - ScrobbleLive-has-zero-test-coverage.md +++ b/backlog/completed/ml-80 - ScrobbleLive-has-zero-test-coverage.md @@ -3,17 +3,18 @@ id: ML-80 title: ScrobbleLive has zero test coverage status: Done 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/95' + - "https://github.com/cloud8421/music_library/issues/95" priority: medium --- ## Description + _GitHub: created 2026-03-05 · updated 2026-03-05 · closed 2026-03-05_ ## Priority: Medium @@ -34,4 +35,5 @@ Add test files covering at least the happy paths for search, selection, and scro ## Source From technical debt audit (2026-03-05). + diff --git a/backlog/completed/ml-81 - Inconsistent-toast-arity-in-form-components.md b/backlog/completed/ml-81 - Inconsistent-toast-arity-in-form-components.md index b2bf938e..4e2669f2 100644 --- a/backlog/completed/ml-81 - Inconsistent-toast-arity-in-form-components.md +++ b/backlog/completed/ml-81 - Inconsistent-toast-arity-in-form-components.md @@ -3,17 +3,18 @@ id: ML-81 title: Inconsistent toast arity in form components status: Done 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/94' + - "https://github.com/cloud8421/music_library/issues/94" priority: medium --- ## Description + _GitHub: created 2026-03-05 · updated 2026-03-05 · closed 2026-03-05_ ## Priority: Medium @@ -23,12 +24,14 @@ _GitHub: created 2026-03-05 · updated 2026-03-05 · closed 2026-03-05_ Form components (LiveComponents) should use `put_toast!/2` (arity 2), but most currently use `put_toast/3` (arity 3, intended for LiveViews): **Using `put_toast/3` (incorrect for LiveComponents):** + - `lib/music_library_web/live/scrobble_rules_live/form.ex:97,112` - `lib/music_library_web/live/online_store_template_live/form.ex:121,136` - `lib/music_library_web/live/scrobbled_tracks_live/form.ex:113` - `lib/music_library_web/live/artist_live/form.ex:271,303` **Using `put_toast!/2` (correct):** + - `lib/music_library_web/live/record_set_live/form.ex:75,88` ## Expected behavior @@ -38,4 +41,5 @@ All form components should use `put_toast!/2` consistently. ## Source From technical debt audit (2026-03-05). + diff --git a/backlog/completed/ml-82 - MaintenanceLive.Index-queries-database-directly.md b/backlog/completed/ml-82 - MaintenanceLive.Index-queries-database-directly.md index c90b7fb3..20e47795 100644 --- a/backlog/completed/ml-82 - MaintenanceLive.Index-queries-database-directly.md +++ b/backlog/completed/ml-82 - MaintenanceLive.Index-queries-database-directly.md @@ -3,17 +3,18 @@ id: ML-82 title: MaintenanceLive.Index queries database directly status: Done 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/93' + - "https://github.com/cloud8421/music_library/issues/93" priority: medium --- ## Description + _GitHub: created 2026-03-05 · updated 2026-03-05 · closed 2026-03-05_ ## Priority: Medium @@ -31,4 +32,5 @@ Extract database queries into a context module (e.g., `MusicLibrary.Maintenance` ## Source From technical debt audit (2026-03-05). + diff --git a/backlog/completed/ml-83 - Duplicated-apply_fallback_index-across-5-LiveViews.md b/backlog/completed/ml-83 - Duplicated-apply_fallback_index-across-5-LiveViews.md index 32eee15d..c28578cf 100644 --- a/backlog/completed/ml-83 - Duplicated-apply_fallback_index-across-5-LiveViews.md +++ b/backlog/completed/ml-83 - Duplicated-apply_fallback_index-across-5-LiveViews.md @@ -3,17 +3,18 @@ id: ML-83 title: Duplicated apply_fallback_index across 5 LiveViews status: Done 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/92' + - "https://github.com/cloud8421/music_library/issues/92" priority: medium --- ## Description + _GitHub: created 2026-03-05 · updated 2026-03-05 · closed 2026-03-05_ ## Priority: Medium @@ -35,4 +36,5 @@ Extract to a shared helper, parameterizing the stream key name. ## Source From technical debt audit (2026-03-05). + diff --git a/backlog/completed/ml-84 - Missing-FK-index-on-record_set_items.record_id.md b/backlog/completed/ml-84 - Missing-FK-index-on-record_set_items.record_id.md index a8e8986a..281d3860 100644 --- a/backlog/completed/ml-84 - Missing-FK-index-on-record_set_items.record_id.md +++ b/backlog/completed/ml-84 - Missing-FK-index-on-record_set_items.record_id.md @@ -3,17 +3,18 @@ id: ML-84 title: Missing FK index on record_set_items.record_id status: Done 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/91' + - "https://github.com/cloud8421/music_library/issues/91" priority: high --- ## Description + _GitHub: created 2026-03-05 · updated 2026-03-05 · closed 2026-03-05_ ## Priority: High @@ -35,4 +36,5 @@ create index(:record_set_items, [:record_id]) ## Source From technical debt audit (2026-03-05). + diff --git a/backlog/completed/ml-85 - Unvalidated-String.to_integer-in-HTTP-endpoints.md b/backlog/completed/ml-85 - Unvalidated-String.to_integer-in-HTTP-endpoints.md index 27ef2e42..fa6e38d8 100644 --- a/backlog/completed/ml-85 - Unvalidated-String.to_integer-in-HTTP-endpoints.md +++ b/backlog/completed/ml-85 - Unvalidated-String.to_integer-in-HTTP-endpoints.md @@ -3,17 +3,18 @@ id: ML-85 title: Unvalidated String.to_integer() in HTTP endpoints status: Done 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/90' + - "https://github.com/cloud8421/music_library/issues/90" priority: high --- ## Description + _GitHub: created 2026-03-05 · updated 2026-03-05 · closed 2026-03-05_ ## Priority: Critical @@ -37,4 +38,5 @@ Use `Integer.parse/1` with proper error handling, similar to `MusicLibraryWeb.Li ## Source From technical debt audit (2026-03-05). + diff --git a/backlog/completed/ml-86 - Scrobble-data-enrichment.md b/backlog/completed/ml-86 - Scrobble-data-enrichment.md index 1b255c40..6a43bf05 100644 --- a/backlog/completed/ml-86 - Scrobble-data-enrichment.md +++ b/backlog/completed/ml-86 - Scrobble-data-enrichment.md @@ -3,18 +3,20 @@ id: ML-86 title: Scrobble data enrichment status: Done 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/88' + - "https://github.com/cloud8421/music_library/issues/88" priority: medium --- ## Description + _GitHub: created 2026-02-17 · updated 2026-02-17 · closed 2026-02-17_ Audit [2026-02-12-look-at-the-scrobbledtracks-table-and-identify.txt](https://github.com/user-attachments/files/25359604/2026-02-12-look-at-the-scrobbledtracks-table-and-identify.txt) + diff --git a/backlog/completed/ml-87 - ArtistInfo.country-1-assumes-map-key-exists.md b/backlog/completed/ml-87 - ArtistInfo.country-1-assumes-map-key-exists.md index 31070ec7..6c57a176 100644 --- a/backlog/completed/ml-87 - ArtistInfo.country-1-assumes-map-key-exists.md +++ b/backlog/completed/ml-87 - ArtistInfo.country-1-assumes-map-key-exists.md @@ -3,17 +3,18 @@ id: ML-87 title: ArtistInfo.country/1 assumes map key exists status: Done 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/87' + - "https://github.com/cloud8421/music_library/issues/87" priority: low --- ## Description + _GitHub: created 2026-02-17 · updated 2026-03-06 · closed 2026-03-06_ ## Priority: Low @@ -29,4 +30,5 @@ Use `Map.get/3` with a default value instead of pattern matching. ## Source From technical debt audit (2026-02-17), item #14. + diff --git a/backlog/completed/ml-88 - Hardcoded-genre-filter-in-collection-stats.md b/backlog/completed/ml-88 - Hardcoded-genre-filter-in-collection-stats.md index e20c078d..ae3b432d 100644 --- a/backlog/completed/ml-88 - Hardcoded-genre-filter-in-collection-stats.md +++ b/backlog/completed/ml-88 - Hardcoded-genre-filter-in-collection-stats.md @@ -3,17 +3,18 @@ id: ML-88 title: Hardcoded genre filter in collection stats status: Done 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/86' + - "https://github.com/cloud8421/music_library/issues/86" priority: low --- ## Description + _GitHub: created 2026-02-17 · updated 2026-03-06 · closed 2026-03-06_ ## Priority: Low @@ -29,4 +30,5 @@ Make the excluded genres configurable rather than hardcoded. ## Source From technical debt audit (2026-02-17), item #13. + diff --git a/backlog/completed/ml-9 - Add-mix_audit-dependency-for-CVE-scanning.md b/backlog/completed/ml-9 - Add-mix_audit-dependency-for-CVE-scanning.md index 1dc470b9..39ad5ffb 100644 --- a/backlog/completed/ml-9 - Add-mix_audit-dependency-for-CVE-scanning.md +++ b/backlog/completed/ml-9 - Add-mix_audit-dependency-for-CVE-scanning.md @@ -3,18 +3,19 @@ id: ML-9 title: Add mix_audit dependency for CVE scanning status: Done assignee: [] -created_date: '2026-04-20 08:49' -updated_date: '2026-04-24 07:13' +created_date: "2026-04-20 08:49" +updated_date: "2026-04-24 07:13" labels: [] dependencies: [] references: - - 'https://github.com/cloud8421/music_library/issues/174' + - "https://github.com/cloud8421/music_library/issues/174" priority: low --- ## Description + _GitHub: created 2026-04-16 · updated 2026-04-16_ ## Summary @@ -39,7 +40,9 @@ mix deps.audit And add a step in `.github/workflows/test_and_deploy.yml` lint job. ## Acceptance Criteria + + - `mix deps.audit` runs in CI - CI fails (or warns, TBD) on advisories @@ -51,14 +54,17 @@ And add a step in `.github/workflows/test_and_deploy.yml` lint job. ## Implementation Notes + Decision on fail-vs-warn: **warn-only** on both CI and local lint. Rationale: CVE databases evolve independently of the project, so a newly published advisory should surface quickly (annotation in CI) without blocking unrelated PRs or breaking `dev:lint` for devs who may not be able to remediate immediately (e.g. waiting on upstream fix). Changes: + - `mix.exs`: added `{:mix_audit, "~> 2.1", only: :dev, runtime: false}` alongside other dev-only quality tools (ex_slop, quokka, credo). - `scripts/dev/lint`: added `mix deps.audit || true` between credo and gettext steps. - `.github/workflows/test_and_deploy.yml`: added `🛡️ Audit dependencies for CVEs` step in the lint job with `continue-on-error: true`, placed after sobelow. Verification: + - `mix deps.get` resolved mix_audit 2.1.x plus yaml_elixir/yamerl transitive deps. - `mix deps.audit` currently reports "No vulnerabilities found." — baseline clean. - `shellcheck` passes on the updated `scripts/dev/lint`. diff --git a/backlog/completed/ml-90 - Credo-complexity-checks-disabled.md b/backlog/completed/ml-90 - Credo-complexity-checks-disabled.md index f72931c0..fc4c3d88 100644 --- a/backlog/completed/ml-90 - Credo-complexity-checks-disabled.md +++ b/backlog/completed/ml-90 - Credo-complexity-checks-disabled.md @@ -3,17 +3,18 @@ id: ML-90 title: Credo complexity checks disabled status: Done 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/84' + - "https://github.com/cloud8421/music_library/issues/84" priority: low --- ## Description + _GitHub: created 2026-02-17 · updated 2026-03-06 · closed 2026-03-06_ ## Priority: Low @@ -29,4 +30,5 @@ Re-enable complexity checks and refactor flagged functions. ## Source From technical debt audit (2026-02-17), item #11. + diff --git a/backlog/completed/ml-91 - ScrobbleActivity-is-a-god-module-557-lines.md b/backlog/completed/ml-91 - ScrobbleActivity-is-a-god-module-557-lines.md index 7b4befcb..4afed071 100644 --- a/backlog/completed/ml-91 - ScrobbleActivity-is-a-god-module-557-lines.md +++ b/backlog/completed/ml-91 - ScrobbleActivity-is-a-god-module-557-lines.md @@ -3,17 +3,18 @@ id: ML-91 title: ScrobbleActivity is a god module (557 lines) status: Done 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/83' + - "https://github.com/cloud8421/music_library/issues/83" priority: medium --- ## Description + _GitHub: created 2026-02-17 · updated 2026-03-02 · closed 2026-03-02_ ## Priority: Medium @@ -29,4 +30,5 @@ Extract cohesive groups of functions into focused modules (e.g., stats, search, ## Source From technical debt audit (2026-02-17), item #10. + diff --git a/backlog/completed/ml-92 - Heavy-duplication-in-ScrobbleRules-context-819-lines.md b/backlog/completed/ml-92 - Heavy-duplication-in-ScrobbleRules-context-819-lines.md index 89ecf813..048f613d 100644 --- a/backlog/completed/ml-92 - Heavy-duplication-in-ScrobbleRules-context-819-lines.md +++ b/backlog/completed/ml-92 - Heavy-duplication-in-ScrobbleRules-context-819-lines.md @@ -3,17 +3,18 @@ id: ML-92 title: Heavy duplication in ScrobbleRules context (819 lines) status: Done 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/82' + - "https://github.com/cloud8421/music_library/issues/82" priority: medium --- ## Description + _GitHub: created 2026-02-17 · updated 2026-03-02 · closed 2026-03-02_ ## Priority: Medium @@ -35,4 +36,5 @@ Consolidate with optional track-filtering parameters to reduce duplication. ## Source From technical debt audit (2026-02-17), item #9. + diff --git a/backlog/completed/ml-93 - inspectreason-exposed-to-users-in-error-messages.md b/backlog/completed/ml-93 - inspectreason-exposed-to-users-in-error-messages.md index 4e4961f9..7d0c30e7 100644 --- a/backlog/completed/ml-93 - inspectreason-exposed-to-users-in-error-messages.md +++ b/backlog/completed/ml-93 - inspectreason-exposed-to-users-in-error-messages.md @@ -3,17 +3,18 @@ id: ML-93 title: inspect(reason) exposed to users in error messages 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/81' + - "https://github.com/cloud8421/music_library/issues/81" priority: medium --- ## Description + _GitHub: created 2026-02-17 · updated 2026-03-05 · closed 2026-03-05_ ## Priority: Medium @@ -36,4 +37,5 @@ Log `inspect(reason)` for debugging but show a generic user-facing message inste ## Source From technical debt audit (2026-02-17), item #8. + diff --git a/backlog/completed/ml-94 - Unsafe-Assets.store_image-pattern-match-in-artist-form.md b/backlog/completed/ml-94 - Unsafe-Assets.store_image-pattern-match-in-artist-form.md index a9327399..2ed24933 100644 --- a/backlog/completed/ml-94 - Unsafe-Assets.store_image-pattern-match-in-artist-form.md +++ b/backlog/completed/ml-94 - Unsafe-Assets.store_image-pattern-match-in-artist-form.md @@ -3,17 +3,18 @@ id: ML-94 title: Unsafe Assets.store_image pattern-match in artist form 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/80' + - "https://github.com/cloud8421/music_library/issues/80" priority: medium --- ## Description + _GitHub: created 2026-02-17 · updated 2026-03-03 · closed 2026-03-03_ ## Priority: Medium @@ -29,4 +30,5 @@ Wrap in `case` or `with` and handle the error tuple to show a user-friendly mess ## Source From technical debt audit (2026-02-17), item #7. + diff --git a/backlog/completed/ml-95 - Process.sleep-for-rate-limiting-in-Oban-workers.md b/backlog/completed/ml-95 - Process.sleep-for-rate-limiting-in-Oban-workers.md index 1b6127c9..616ef9aa 100644 --- a/backlog/completed/ml-95 - Process.sleep-for-rate-limiting-in-Oban-workers.md +++ b/backlog/completed/ml-95 - Process.sleep-for-rate-limiting-in-Oban-workers.md @@ -3,17 +3,18 @@ id: ML-95 title: Process.sleep for rate limiting in Oban workers 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/79' + - "https://github.com/cloud8421/music_library/issues/79" priority: medium --- ## Description + _GitHub: created 2026-02-17 · updated 2026-03-02 · closed 2026-03-02_ ## Priority: Medium @@ -22,11 +23,11 @@ _GitHub: created 2026-02-17 · updated 2026-03-02 · closed 2026-03-02_ Three workers use `Process.sleep` to rate-limit API calls: -| Worker | File | Sleep | -| --- | --- | --- | -| `ArtistRefreshMusicBrainzData` | `lib/music_library/worker/artist_refresh_music_brainz_data.ex:8` | 500ms | -| `ArtistRefreshDiscogsData` | `lib/music_library/worker/artist_refresh_discogs_data.ex:8` | 1000ms | -| `RecordRefreshMusicBrainzData` | `lib/music_library/worker/record_refresh_music_brainz_data.ex:12` | 500ms | +| Worker | File | Sleep | +| ------------------------------ | ----------------------------------------------------------------- | ------ | +| `ArtistRefreshMusicBrainzData` | `lib/music_library/worker/artist_refresh_music_brainz_data.ex:8` | 500ms | +| `ArtistRefreshDiscogsData` | `lib/music_library/worker/artist_refresh_discogs_data.ex:8` | 1000ms | +| `RecordRefreshMusicBrainzData` | `lib/music_library/worker/record_refresh_music_brainz_data.ex:12` | 500ms | This blocks the worker process during the sleep, preventing Oban from using it for other jobs. @@ -37,4 +38,5 @@ Consider using Oban's `rate_limit` plugin or queue-level concurrency limits inst ## Source From technical debt audit (2026-02-17), item #6. + diff --git a/backlog/completed/ml-96 - Missing-schema-validations-on-string-fields.md b/backlog/completed/ml-96 - Missing-schema-validations-on-string-fields.md index bbe714f1..de85dc33 100644 --- a/backlog/completed/ml-96 - Missing-schema-validations-on-string-fields.md +++ b/backlog/completed/ml-96 - Missing-schema-validations-on-string-fields.md @@ -3,17 +3,18 @@ id: ML-96 title: Missing schema validations on string fields 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/78' + - "https://github.com/cloud8421/music_library/issues/78" priority: medium --- ## Description + _GitHub: created 2026-02-17 · updated 2026-03-05 · closed 2026-03-05_ ## Priority: Medium @@ -29,4 +30,5 @@ Add appropriate length validations to string fields across all schemas. ## Source From technical debt audit (2026-02-17), item #5. + diff --git a/backlog/completed/ml-97 - Duplicate-fixture-definitions-in-tests-partially-solved.md b/backlog/completed/ml-97 - Duplicate-fixture-definitions-in-tests-partially-solved.md index 38cb91d4..a5c7f1ea 100644 --- a/backlog/completed/ml-97 - Duplicate-fixture-definitions-in-tests-partially-solved.md +++ b/backlog/completed/ml-97 - Duplicate-fixture-definitions-in-tests-partially-solved.md @@ -3,17 +3,18 @@ id: ML-97 title: Duplicate fixture definitions in tests (partially solved) 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/77' + - "https://github.com/cloud8421/music_library/issues/77" priority: low --- ## Description + _GitHub: created 2026-02-17 · updated 2026-03-06 · closed 2026-03-06_ ## Priority: Medium @@ -31,4 +32,5 @@ Move `scrobbled_track_fixture` to a shared fixture module for consistency. ## Source From technical debt audit (2026-02-17), item #4. + diff --git a/backlog/completed/ml-98 - Potential-nil-crash-in-ScrobbleActivity.md b/backlog/completed/ml-98 - Potential-nil-crash-in-ScrobbleActivity.md index dff3117d..f2136d19 100644 --- a/backlog/completed/ml-98 - Potential-nil-crash-in-ScrobbleActivity.md +++ b/backlog/completed/ml-98 - Potential-nil-crash-in-ScrobbleActivity.md @@ -3,17 +3,18 @@ id: ML-98 title: Potential nil crash in ScrobbleActivity 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/76' + - "https://github.com/cloud8421/music_library/issues/76" priority: medium --- ## Description + _GitHub: created 2026-02-17 · updated 2026-03-05 · closed 2026-03-05_ ## Priority: Medium @@ -29,4 +30,5 @@ Add nil guards or handle the case where `Enum.find` returns `nil` gracefully. ## Source From technical debt audit (2026-02-17), item #3. + diff --git a/backlog/completed/ml-99 - Raw-SQL-with-string-concatenation-in-ScrobbleRules.md b/backlog/completed/ml-99 - Raw-SQL-with-string-concatenation-in-ScrobbleRules.md index 1b5d13e1..edf78c82 100644 --- a/backlog/completed/ml-99 - Raw-SQL-with-string-concatenation-in-ScrobbleRules.md +++ b/backlog/completed/ml-99 - Raw-SQL-with-string-concatenation-in-ScrobbleRules.md @@ -3,17 +3,18 @@ id: ML-99 title: Raw SQL with string concatenation in ScrobbleRules 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/75' + - "https://github.com/cloud8421/music_library/issues/75" priority: medium --- ## Description + _GitHub: created 2026-02-17 · updated 2026-03-05 · closed 2026-03-05_ ## Priority: Medium @@ -29,4 +30,5 @@ Use Ecto query builders or a safer SQL construction approach to improve maintain ## Source From technical debt audit (2026-02-17), item #2. + diff --git a/backlog/docs/doc-1 - Alternatives-for-reducing-collection-chat-token-usage.md b/backlog/docs/doc-1 - Alternatives-for-reducing-collection-chat-token-usage.md index e08e9372..a4c62637 100644 --- a/backlog/docs/doc-1 - Alternatives-for-reducing-collection-chat-token-usage.md +++ b/backlog/docs/doc-1 - Alternatives-for-reducing-collection-chat-token-usage.md @@ -2,9 +2,10 @@ id: doc-1 title: Alternatives for reducing collection chat token usage type: other -created_date: '2026-05-02 16:12' -updated_date: '2026-05-04 06:55' +created_date: "2026-05-02 16:12" +updated_date: "2026-05-04 06:55" --- + # ML-156 Research: Alternatives for reducing collection chat token usage Research document for [ML-156 - Explore alternatives to reduce token usage when providing collection context to LLM for collection chat](backlog://task/ML-156). @@ -14,11 +15,12 @@ Research document for [ML-156 - Explore alternatives to reduce token usage when ## Current state **Token flow per new collection chat:** + 1. `Collection.collection_summary/0` runs on mount via `start_async` 2. Loads ALL records: `from(r in Record, where: not is_nil(r.purchased_at), order_by: [order_alphabetically()], select: ^essential_fields())` 3. Groups by `musicbrainz_id`, formats each group as `"Artist - Title (year, formats) [genre1, genre2]"` 4. Builds stats header: `"# Stats: N releases, M artists\nGenres: ...\nFormats: ...\nEras: ..."` -5. Returns `{stats + "\n\n" + catalog, group_count}` +5. Returns `{stats + "\n\n" + catalog, group_count}` 6. Stored in `@collection_summary` assign on the LiveView 7. Passed to Chat component as `chat_context={@collection_summary}` 8. When user sends first message, `do_send_message` calls `chat_module.stream_response(messages, chat_context, callback)` @@ -30,6 +32,7 @@ Research document for [ML-156 - Explore alternatives to reduce token usage when **For 500 releases:** ~9,000 input tokens for catalog alone + ~100 tokens for stats + ~500 tokens for prompt template = ~9,600 tokens **Key files:** + - `lib/music_library/collection.ex:203-235` — `collection_summary/0` (loads and formats all records) - `lib/music_library/chats/collection_chat.ex:18-31` — `build_instructions/2` (embeds summary in prompt) - `lib/music_library/chats/prompt.ex` — `Prompt.build/2` (wraps in identity + approach) @@ -41,6 +44,7 @@ Research document for [ML-156 - Explore alternatives to reduce token usage when ### Streaming architecture constraints The Chat component dispatches streaming to a `Task.Supervisor` child: + ```elixir Task.Supervisor.start_child(MusicLibrary.TaskSupervisor, fn -> case chat_module.stream_response(stream_messages, chat_context, fn chunk -> @@ -65,17 +69,20 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac **Token savings:** ~9,000 → ~100 input tokens (~99% reduction) **Pros:** + - Simplest possible change; < 10 lines of code - No architectural changes needed - No streaming infrastructure changes - The LLM can still answer statistical questions ("what's my most common genre?", "how many jazz records do I have?") **Cons:** + - LLM loses ability to answer specific questions ("do I have Kid A?", "which Radiohead albums do I own?", "show me my 90s electronic albums") - User experience degrades for record-specific queries - The LLM will hallucinate or say "I don't have access to your specific collection" frequently **Impact on code:** + 1. `CollectionChat.build_instructions/2` — remove `#{collection_summary}` interpolation, keep only stats 2. `Collection.collection_summary/0` — could be simplified to return only stats (or keep as-is, the function is also tested independently) @@ -88,7 +95,9 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac **Token savings:** ~9,000 → ~100 input tokens base + tool call overhead + tool results (~200-500 tokens when actually searching) **How it works:** + 1. Add a tool definition to the Responses API request: + ```json { "type": "function", @@ -97,7 +106,7 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac "parameters": { "type": "object", "properties": { - "query": {"type": "string", "description": "Search query"} + "query": { "type": "string", "description": "Search query" } }, "required": ["query"] } @@ -114,6 +123,7 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac 3. The Chat component's streaming architecture needs to handle this multi-turn flow. **Pros:** + - Maximal token efficiency — only pay for what's actually needed - LLM can answer arbitrary specific questions with real data - Scales to any collection size @@ -121,6 +131,7 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac - Leverages the existing `tools` infrastructure in the Responses API request **Cons:** + - **Significantly more complex** — requires: - Changes to `OpenAI.API.chat_stream/6` to support function calls in streaming mode - Changes to `decode_responses_event/2` to handle function call SSE events @@ -132,6 +143,7 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac - More error states to handle (function execution failures, parse errors) **Implementation scope:** + 1. **Tool definition module** — New module ~30 lines defining the OpenAI function tool schema 2. **Function executor** — New module or function in `CollectionChat` that executes the tool call (~20 lines) 3. **Streaming changes in `OpenAI.API`** — New `chat_stream_with_tools/7` or modify `chat_stream/6` to accept tools and handle function call events (~80 lines) @@ -150,12 +162,14 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac **Token savings:** ~9,000 → ~400 tokens (summary text + stats) **Pros:** + - Good middle ground — compact but informative - The LLM has a narrative understanding of the collection - No streaming architecture changes needed - Single one-time cost to generate (amortized across many chats) **Cons:** + - Still doesn't give the LLM ability to answer specific queries ("do I have Kid A?") - Summary can go stale if not regenerated on collection changes - Requires an initial LLM call to generate the summary (token cost + latency) @@ -164,6 +178,7 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac - Adds a new background job / async concern **Implementation scope:** + 1. Store the cached summary (new DB field on a canonical collection `Chat` record, or a new schema) 2. A function/worker to generate the LLM summary (calls OpenAI, stores result) 3. Invalidation triggers (PubSub on record add/edit/delete, regenerate async) @@ -179,11 +194,13 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac **Token savings:** ~9,000 → ~100 tokens base + tool results (0-500 tokens per use) **Pros:** + - Best of both worlds: statistical awareness always available, specific lookup on demand - Token-efficient — base cost is minimal - LLM knows to reach for the tool when appropriate **Cons:** + - Same implementation complexity as Alternative B for the tool infrastructure - Slightly more prompt engineering to ensure the model uses the tool appropriately @@ -198,6 +215,7 @@ Upload the collection catalog as a file to OpenAI, create a vector store, and us **Token savings:** ~9,000 → ~100 tokens base + retrieval overhead (~200-500 when model searches) **How it works:** + 1. Format collection catalog as text (same as current `collection_summary/0` output) 2. Upload to OpenAI: `POST /v1/files` with `purpose: "assistants"` → `file_id` 3. Create vector store: `POST /v1/vector_stores` → `vector_store_id` @@ -208,6 +226,7 @@ Upload the collection catalog as a file to OpenAI, create a vector store, and us **Critical difference from Alt B:** No SSE event handling changes, no orchestration loop, no custom tool execution. `file_search` works exactly like `web_search_preview` (already in use) — OpenAI handles retrieval automatically. **Pros:** + - ~150 lines vs ~250 for Alt B - No changes to `decode_responses_event/2` or Chat streaming loop - Semantic search (finds "upbeat 80s rock" not just keywords) @@ -215,6 +234,7 @@ Upload the collection catalog as a file to OpenAI, create a vector store, and us - Reusable for artist bios, notes, etc. **Cons:** + - File can go stale if not updated on collection change - 2-3 new API endpoints needed (Files API, Vector Stores API) - Vector store indexing is async (may need brief poll) @@ -226,13 +246,13 @@ Upload the collection catalog as a file to OpenAI, create a vector store, and us ## Comparison -| Criterion | A (stats-only) | B (custom func) | C (cached) | E (file_search) | -|---|---|---|---|---| -| Implementation complexity | ★☆☆☆☆ | ★★★★☆ | ★★★☆☆ | ★★☆☆☆ | -| Response quality | ★★☆☆☆ | ★★★★★ | ★★★☆☆ | ★★★★★ | -| Token efficiency | ★★★★★ | ★★★★★ | ★★★★☆ | ★★★★★ | -| Infrastructure risk | ★★★★★ | ★★★☆☆ | ★★★★☆ | ★★★★☆ | -| Reusability | ★☆☆☆☆ | ★★★★☆ | ★★☆☆☆ | ★★★☆☆ | +| Criterion | A (stats-only) | B (custom func) | C (cached) | E (file_search) | +| ------------------------- | -------------- | --------------- | ---------- | --------------- | +| Implementation complexity | ★☆☆☆☆ | ★★★★☆ | ★★★☆☆ | ★★☆☆☆ | +| Response quality | ★★☆☆☆ | ★★★★★ | ★★★☆☆ | ★★★★★ | +| Token efficiency | ★★★★★ | ★★★★★ | ★★★★☆ | ★★★★★ | +| Infrastructure risk | ★★★★★ | ★★★☆☆ | ★★★★☆ | ★★★★☆ | +| Reusability | ★☆☆☆☆ | ★★★★☆ | ★★☆☆☆ | ★★★☆☆ | --- diff --git a/backlog/docs/doc-2 - Prevent-pi-from-accessing-sensitive-files.md b/backlog/docs/doc-2 - Prevent-pi-from-accessing-sensitive-files.md index a9b5f07a..a0ae4ddb 100644 --- a/backlog/docs/doc-2 - Prevent-pi-from-accessing-sensitive-files.md +++ b/backlog/docs/doc-2 - Prevent-pi-from-accessing-sensitive-files.md @@ -2,9 +2,10 @@ id: doc-2 title: Prevent pi from accessing sensitive files type: other -created_date: '2026-05-03 13:32' -updated_date: '2026-05-04 06:55' +created_date: "2026-05-03 13:32" +updated_date: "2026-05-04 06:55" --- + # Implementation Analysis: Prevent pi from accessing sensitive files ## Summary @@ -13,14 +14,14 @@ The task requires a **declarative** way to intercept pi tool calls that would ac ## Sensitive File Categories (for this project) -| Category | Patterns | Examples | -|----------|----------|----------| -| Environment secrets | `.env`, `.env.*`, `.envrc` | `.env`, `.env.production` | -| Key files | `*.pem`, `*.key`, `*.key.pub` | SSH keys | -| Secret/credential files | `*secret*`, `*credential*`, `*credentials*` | `secrets.yml`, `credentials.json` | -| Config dirs | `.ssh/`, `.aws/`, `.gnupg/` | SSH config, AWS credentials | -| Production config | Any path known to contain secrets | `rel/`, production env vars | -| Pi session files | `~/.pi/agent/sessions/` | Session JSONL files (contain tool outputs) | +| Category | Patterns | Examples | +| ----------------------- | ------------------------------------------- | ------------------------------------------ | +| Environment secrets | `.env`, `.env.*`, `.envrc` | `.env`, `.env.production` | +| Key files | `*.pem`, `*.key`, `*.key.pub` | SSH keys | +| Secret/credential files | `*secret*`, `*credential*`, `*credentials*` | `secrets.yml`, `credentials.json` | +| Config dirs | `.ssh/`, `.aws/`, `.gnupg/` | SSH config, AWS credentials | +| Production config | Any path known to contain secrets | `rel/`, production env vars | +| Pi session files | `~/.pi/agent/sessions/` | Session JSONL files (contain tool outputs) | ## Available Pi Extension Mechanisms @@ -37,6 +38,7 @@ pi.on("tool_call", async (event, ctx) => { ``` **Key capabilities:** + - `event.toolName` — identifies the tool (`read`, `bash`, `grep`, `find`, `ls`, `write`, `edit`) - `event.input` — tool parameters (mutable, can be patched) - `isToolCallEventType("read", event)` — type-narrows for specific tools @@ -46,15 +48,15 @@ pi.on("tool_call", async (event, ctx) => { ### Tools that can access sensitive files -| Tool | Danger | Input fields to check | -|------|--------|----------------------| -| `read` | **High** — reads file content into LLM context | `input.path` | -| `bash` | **High** — can `cat`, `grep`, `curl` any file | `input.command` (regex match) | -| `grep` | **High** — searches file contents | `input.path` | -| `find` | **Low** — lists filenames only | `input.path` (directory listing) | -| `ls` | **Low** — lists directory contents | `input.path` | -| `write` | **Medium** — could overwrite sensitive files | `input.path` | -| `edit` | **Medium** — could modify sensitive files | `input.path` | +| Tool | Danger | Input fields to check | +| ------- | ---------------------------------------------- | -------------------------------- | +| `read` | **High** — reads file content into LLM context | `input.path` | +| `bash` | **High** — can `cat`, `grep`, `curl` any file | `input.command` (regex match) | +| `grep` | **High** — searches file contents | `input.path` | +| `find` | **Low** — lists filenames only | `input.path` (directory listing) | +| `ls` | **Low** — lists directory contents | `input.path` | +| `write` | **Medium** — could overwrite sensitive files | `input.path` | +| `edit` | **Medium** — could modify sensitive files | `input.path` | ### `tool_result` event (secondary approach) @@ -85,6 +87,7 @@ pi.registerTool({ **Description:** Single extension listening on `tool_call`, checking paths/commands against a configuration file of protected patterns. Blocks matching calls. **Implementation:** + 1. Create `.pi/extensions/sensitive-file-guard.ts` (or `index.ts` in a directory) 2. Create `.pi/sensitive-paths.json` — declarative config listing protected patterns 3. In `tool_call` handler: @@ -94,6 +97,7 @@ pi.registerTool({ - Block if match, notify in UI **Sample config file (`.pi/sensitive-paths.json`):** + ```json { "blocked_paths": [ @@ -109,16 +113,12 @@ pi.registerTool({ ".gnupg/", "config/*.secret.exs" ], - "blocked_commands": [ - "cat .env", - "cat ~/.ssh", - "printenv", - "cat ~/.aws" - ] + "blocked_commands": ["cat .env", "cat ~/.ssh", "printenv", "cat ~/.aws"] } ``` **Pros:** + - Minimal code (~60 lines of TypeScript) - Declarative configuration file (easy to audit and update) - Works with ALL tools (read, bash, grep, find, ls, write, edit) @@ -128,6 +128,7 @@ pi.registerTool({ - Fails safe (blocks before any file access) **Cons:** + - Can't sanitize partial output (but blocking prevents all access) - Regex-based command scanning for bash has edge cases - `find`/`ls` only blocked at directory level, not per-file @@ -141,17 +142,20 @@ pi.registerTool({ **Description:** Override the `read` and `bash` tools with custom implementations that include path checking. Delegates to original implementation for allowed paths. **Implementation:** + 1. Create `.pi/extensions/sensitive-file-guard/` directory 2. Override `read` tool: check paths, delegate allowed reads to original implementation 3. Override `bash` tool: scan commands, delegate allowed commands to original implementation 4. Optionally override `grep`, `find`, `ls`, `write`, `edit` **Pros:** + - Full control (can log, can sanitize output, can modify behavior) - No event overhead per tool call - Can add audit logging **Cons:** + - Must reimplement or re-wrap built-in tool logic (~200+ lines) - Must maintain compatibility with pi updates - More complex testing @@ -170,10 +174,12 @@ pi.registerTool({ Same as Route A, plus a `tool_result` handler that scans output for sensitive patterns and redacts them. **Pros:** + - Most comprehensive protection - Can allow partial access with sanitization **Cons:** + - Most complex - `tool_result` sanitization is error-prone (false positives/negatives) - Redacted content still consumes context tokens @@ -195,14 +201,14 @@ Route A (pure `tool_call` event interception with declarative config) is the **s ## Architectural Impact -| Touchpoint | Impact | -|------------|--------| -| `.pi/extensions/` | New extension file added | -| `.pi/sensitive-paths.json` | New config file (declarative rules) | -| `tool_call` event | New subscriber, no breaking changes | -| Built-in tools | Unchanged (event interception is non-invasive) | -| `ctx.ui` | Used for notification only (no blocking dependency) | -| Other extensions | Compatible — tool_call handlers chain in load order | +| Touchpoint | Impact | +| -------------------------- | --------------------------------------------------- | +| `.pi/extensions/` | New extension file added | +| `.pi/sensitive-paths.json` | New config file (declarative rules) | +| `tool_call` event | New subscriber, no breaking changes | +| Built-in tools | Unchanged (event interception is non-invasive) | +| `ctx.ui` | Used for notification only (no blocking dependency) | +| Other extensions | Compatible — tool_call handlers chain in load order | ## Open Questions diff --git a/backlog/docs/doc-3 - Force-production-logs-to-single-line-format.md b/backlog/docs/doc-3 - Force-production-logs-to-single-line-format.md index 89be24c3..29fe58f2 100644 --- a/backlog/docs/doc-3 - Force-production-logs-to-single-line-format.md +++ b/backlog/docs/doc-3 - Force-production-logs-to-single-line-format.md @@ -2,14 +2,16 @@ id: doc-3 title: Force production logs to single-line format type: other -created_date: '2026-05-03 13:53' -updated_date: '2026-05-04 06:55' +created_date: "2026-05-03 13:53" +updated_date: "2026-05-04 06:55" --- + # Research: Force production logs to single-line format ## Current state **Config** (`config/config.exs`): + ```elixir config :logger, :default_formatter, format: "$time $metadata[$level] $message\n", @@ -17,18 +19,19 @@ config :logger, :default_formatter, ``` **Override** (`config/prod.exs`): + ```elixir config :logger, level: :info ``` **Log sources in prod**: -| Source | Produces | Problem | -|---|---|---| -| `Phoenix.Logger` (telemetry) | `GET /health` + `Sent 200 in Xms` | Two separate lines for one request — can't reverse | -| `Phoenix.Logger` (telemetry) | `CONNECTED TO Phoenix.LiveView.Socket...` | One event with embedded `\n` — 4+ lines | -| Custom `Logger.info/error/...` | Varies | Some may be multi-line | -| OTP / stack traces | Crash reports | Multi-line | +| Source | Produces | Problem | +| ------------------------------ | ----------------------------------------- | -------------------------------------------------- | +| `Phoenix.Logger` (telemetry) | `GET /health` + `Sent 200 in Xms` | Two separate lines for one request — can't reverse | +| `Phoenix.Logger` (telemetry) | `CONNECTED TO Phoenix.LiveView.Socket...` | One event with embedded `\n` — 4+ lines | +| Custom `Logger.info/error/...` | Varies | Some may be multi-line | +| OTP / stack traces | Crash reports | Multi-line | `Phoenix.Logger` is auto-attached by Phoenix.Endpoint and handles telemetry events including `[:phoenix, :endpoint, :start]`, `[:phoenix, :endpoint, :stop]`, and `[:phoenix, :socket_connected]`. @@ -46,14 +49,14 @@ v2 (`~> 2.0.0-rc.5`) uses `Logster.attach_phoenix_logger()` in `application.ex` ### Architecture impact -| Touchpoint | Change | -|---|---| -| `config/prod.exs` | Add `config :phoenix, :logger, false` + Logster config + formatter config | -| `lib/music_library/application.ex` | Add conditional `Logster.attach_phoenix_logger()` | -| `lib/music_library_web/telemetry_log_handler.ex` | **New module** — handles socket_connected telemetry | -| `lib/music_library/logger/single_line_formatter.ex` | **New module** — Logger.Formatter format function | -| `mix.exs` | Add `{:logster, "~> 2.0.0-rc.5"}` | -| Docs | Update `docs/production-infrastructure.md` with logging config | +| Touchpoint | Change | +| --------------------------------------------------- | ------------------------------------------------------------------------- | +| `config/prod.exs` | Add `config :phoenix, :logger, false` + Logster config + formatter config | +| `lib/music_library/application.ex` | Add conditional `Logster.attach_phoenix_logger()` | +| `lib/music_library_web/telemetry_log_handler.ex` | **New module** — handles socket_connected telemetry | +| `lib/music_library/logger/single_line_formatter.ex` | **New module** — Logger.Formatter format function | +| `mix.exs` | Add `{:logster, "~> 2.0.0-rc.5"}` | +| Docs | Update `docs/production-infrastructure.md` with logging config | ### Performance diff --git a/backlog/docs/doc-4 - GitHub-Actions-→-Tangled-Spindles.md b/backlog/docs/doc-4 - GitHub-Actions-→-Tangled-Spindles.md index 2bb688c7..d95ba9ed 100644 --- a/backlog/docs/doc-4 - GitHub-Actions-→-Tangled-Spindles.md +++ b/backlog/docs/doc-4 - GitHub-Actions-→-Tangled-Spindles.md @@ -2,9 +2,10 @@ id: doc-4 title: GitHub Actions → Tangled Spindles type: other -created_date: '2026-05-03 16:57' -updated_date: '2026-05-04 06:54' +created_date: "2026-05-03 16:57" +updated_date: "2026-05-04 06:54" --- + # CI Pipeline Porting Analysis: GitHub Actions → Tangled Spindles ## Current State (GitHub Actions) @@ -22,6 +23,7 @@ Key GHA features used: `jdx/mise-action`, `actions/cache`, concurrency control, Tangled provides **Spindles** — a CI runner service. Workflows are YAML files in `.tangled/workflows/`. **Available features:** + - Triggers: `push`, `pull_request`, `manual` — with branch/tag glob filtering - Engine: `nixery` (uses Nixpkgs packages via Nixery at `nixery.tangled.sh`) - Dependencies: declared from nixpkgs (or custom registries) @@ -34,6 +36,7 @@ Tangled provides **Spindles** — a CI runner service. Workflows are YAML files - Timeout: configurable via `SPINDLE_PIPELINES_WORKFLOW_TIMEOUT` (default 5m) **Missing features (vs GitHub Actions):** + - ❌ No job dependencies (`needs`) — all steps in a workflow are strictly sequential - ❌ No parallel jobs — single linear pipeline per workflow file - ❌ No inter-run caching primitive — no equivalent to `actions/cache` @@ -51,20 +54,20 @@ Tangled also provides **Webhooks** (push events, HMAC-signed, 3 retries with bac Nixery builds OCI images with each nixpkgs package in a **separate Docker layer**. Docker reuses pre-existing layers, so any nixpkgs dependency declared in the `dependencies` block — `mise`, `nodejs`, `shellcheck`, `hurl`, `sqlite` — is cached at the Docker level between runs. -This is confirmed by the Tangled team: *"caching for commonly used packages is free thanks to Docker (pre-existing layers get reused)."* — [Introducing Spindle blog post](https://blog.tangled.org/ci/) +This is confirmed by the Tangled team: _"caching for commonly used packages is free thanks to Docker (pre-existing layers get reused)."_ — [Introducing Spindle blog post](https://blog.tangled.org/ci/) ### What is NOT cached (fetched/compiled fresh every run) -| Artifact | Reason | -|----------|--------| -| Elixir 1.20.0-rc.4, Erlang 28.5 | Not in nixpkgs; `mise install` downloads fresh each run | -| `mix deps` + `_build` | No directory-level cache primitive in Spindle | -| `npm` packages + asset builds | Same — no cache primitive | -| Fluxon private Hex repo packages | Fetched each run | +| Artifact | Reason | +| -------------------------------- | ------------------------------------------------------- | +| Elixir 1.20.0-rc.4, Erlang 28.5 | Not in nixpkgs; `mise install` downloads fresh each run | +| `mix deps` + `_build` | No directory-level cache primitive in Spindle | +| `npm` packages + asset builds | Same — no cache primitive | +| Fluxon private Hex repo packages | Fetched each run | ### Within a run -Per the blog post: *"the `/tangled/workspace` and `/nix` volumes persisted across steps."* So within a single workflow run, compiled artifacts are available to subsequent steps. But between runs, everything resets except the Nixery Docker layers. +Per the blog post: _"the `/tangled/workspace` and `/nix` volumes persisted across steps."_ So within a single workflow run, compiled artifacts are available to subsequent steps. But between runs, everything resets except the Nixery Docker layers. ### Potential caching strategy: Custom Nix derivation for Elixir 1.20.0-rc.4 @@ -74,20 +77,21 @@ Similarly, Erlang 28.5 could be pinned via a custom derivation if not in nixpkgs ## Dependency Availability in Nixpkgs -| Tool | In nixpkgs stable? | Version | Notes | -|------|-------------------|---------|-------| -| `elixir` | ✅ | 1.18.4 / 1.19.5 | **Not 1.20.0-rc.4** — must use mise or custom Nix derivation | -| `erlang` | ✅ | via beam packages | OTP versions tied to package sets | -| `nodejs` | ✅ | Yes | | -| `mise` | ✅ | 2025.11.7 | Can install exact Elixir/Erlang/Node from mise.toml | -| `shellcheck` | ✅ | Yes | | -| `hurl` | ✅ (likely) | | | -| `sqlite` | ✅ | Yes | | -| `credo` | ❌ | Elixir mix task | Must install via mix after Elixir is available | -| `sobelow` | ❌ | Elixir mix task | Same | -| `mix_audit` | ❌ | Elixir mix task | Same | +| Tool | In nixpkgs stable? | Version | Notes | +| ------------ | ------------------ | ----------------- | ------------------------------------------------------------ | +| `elixir` | ✅ | 1.18.4 / 1.19.5 | **Not 1.20.0-rc.4** — must use mise or custom Nix derivation | +| `erlang` | ✅ | via beam packages | OTP versions tied to package sets | +| `nodejs` | ✅ | Yes | | +| `mise` | ✅ | 2025.11.7 | Can install exact Elixir/Erlang/Node from mise.toml | +| `shellcheck` | ✅ | Yes | | +| `hurl` | ✅ (likely) | | | +| `sqlite` | ✅ | Yes | | +| `credo` | ❌ | Elixir mix task | Must install via mix after Elixir is available | +| `sobelow` | ❌ | Elixir mix task | Same | +| `mix_audit` | ❌ | Elixir mix task | Same | The workflow pattern with mise: + 1. Add `mise` as a nixpkgs dependency (cached via Docker layers ✅) 2. `mise install` to fetch Elixir 1.20.0-rc.4, Erlang 28.5, Node 25.9.0 (NOT cached ❌) 3. Run existing mise tasks @@ -95,30 +99,35 @@ The workflow pattern with mise: ## Implementation Routes ### Route A: Pure Spindle — Single Linear Workflow + One `.tangled/workflows/ci.yml` that runs lint → test → deploy sequentially. **Pros:** Simplest — one file, fully self-contained on Tangled. **Cons:** Slowest (no parallelism between lint+test), deploy runs on every PR push to main, no separation of concerns. ### Route B: Multiple Spindle Workflows — Split CI/CD + Separate files: `lint.yml`, `test.yml`, `deploy.yml`. Deploy is manual-trigger only. **Pros:** Lint and test can trigger on PRs, deploy is intentional. Better separation of concerns. **Cons:** No enforcement that test passes before manual deploy. Lint and test still run sequentially within each workflow. ### Route C: Spindle for CI + Webhook for Deploy + Spindle handles lint+test on PRs. A Tangled webhook on push to main triggers Coolify deployment via an external endpoint. **Pros:** Deploy runs independently. Can add gating logic in webhook receiver. **Cons:** Requires a webhook receiver (external service). More infrastructure. ### Route D: Spindle for All — Lean Pipeline (Accept Gaps) + Full pipeline on Spindle but accept the missing features. Configure longer timeout, accept sequential runs, use manual deploy trigger. **Pros:** Fully native Tangled experience. **Cons:** All gaps accepted as-is. May frustrate PR workflow. ### Route E: Mirror to GitHub + Keep Actions + Mirror the Tangled repo to GitHub (or use Tangled as a mirror of GitHub) and keep existing Actions. **Pros:** Zero CI changes. All existing features preserved. diff --git a/backlog/docs/doc-5 - Log-Line-Copy-in-Prod-Logs-Extension.md b/backlog/docs/doc-5 - Log-Line-Copy-in-Prod-Logs-Extension.md index 058a09a2..d920c0be 100644 --- a/backlog/docs/doc-5 - Log-Line-Copy-in-Prod-Logs-Extension.md +++ b/backlog/docs/doc-5 - Log-Line-Copy-in-Prod-Logs-Extension.md @@ -2,9 +2,10 @@ id: doc-5 title: Log Line Copy in Prod-Logs Extension type: other -created_date: '2026-05-03 21:06' -updated_date: '2026-05-04 06:55' +created_date: "2026-05-03 21:06" +updated_date: "2026-05-04 06:55" --- + # Implementation Analysis: Log Line Copy in Prod-Logs Extension ## Problem Summary @@ -28,13 +29,15 @@ The `/prod-logs` extension (`.pi/extensions/prod-logs/index.ts`) displays log li ## Route A: Cursor + Toggle Selection + setEditorText ### Concept + Add a visual cursor line. Enter to copy single line. Space to toggle line selection (multi-select mode). `y` to copy all selected lines and exit. After exit, call `ctx.ui.setEditorText()` with the copied text. ### Changes to LogViewer + 1. Add `cursorIndex: number` (absolute line index into `this.lines`) 2. Add `selectedIndices: Set` (absolute indices) 3. New key bindings: - - `Enter` → copy cursor line, `done(line)` + - `Enter` → copy cursor line, `done(line)` - `Space` → toggle selection of cursor line - `y` → copy selected lines (or cursor line if none selected), `done(text)` 4. Render changes: @@ -44,6 +47,7 @@ Add a visual cursor line. Enter to copy single line. Space to toggle line select 5. Help text update: add "y copy · Space select · Enter copy line" ### Return value handling + ```typescript const copiedText = await ctx.ui.custom(...); if (copiedText !== null) { @@ -52,12 +56,14 @@ if (copiedText !== null) { ``` ### Pros + - No external dependencies - `setEditorText` is already available in pi's ExtensionAPI - Aligns with the "paste" use case (text in editor = ready to send or copy) - Simple, contained change to one file ### Cons + - Modifies editor content (replaces whatever the user had typed) - Not a true "system clipboard" copy — the user can't paste outside pi without an extra step - Toggle-based selection for multi-line is somewhat unusual UX (vs. range selection) @@ -67,9 +73,11 @@ if (copiedText !== null) { ## Route B: Visual Mode (Vim-style) + setEditorText ### Concept + Vim-inspired visual mode: `v` enters linewise visual selection, `j`/`k` extend the range, `y` copies and exits. Enter still copies single line. Selection is always a contiguous range. ### Changes to LogViewer + 1. Add `cursorIndex: number` (absolute) 2. Add `visualMode: boolean` + `visualAnchor: number` (start of visual selection) 3. New key bindings: @@ -84,11 +92,13 @@ Vim-inspired visual mode: `v` enters linewise visual selection, `j`/`k` extend t 5. Help text: show visual mode help when active ### Pros + - Familiar UX for vim users - Contiguous range selection is intuitive - Same `setEditorText` approach — no external deps ### Cons + - Slightly more complex state machine (normal mode vs. visual mode) - Doesn't allow non-contiguous selection (but the user said "contiguous") - Still modifies editor content @@ -98,19 +108,23 @@ Vim-inspired visual mode: `v` enters linewise visual selection, `j`/`k` extend t ## Route C: System Clipboard + Either Selection Model ### Concept + Either selection model (A or B), but instead of `setEditorText`, write to the system clipboard. Requires a package like `clipboardy`. ### Additional Changes + - Add `clipboardy` dependency (or use `node:child_process` with `pbcopy`/`xclip`) - On copy: `clipboardy.writeSync(text)` or similar - No need to change return type — can stay `void` since we don't pass text back ### Pros + - True "copy" behavior — paste anywhere - Doesn't touch editor content - Works across applications ### Cons + - External dependency (`clipboardy` — native module, may have install issues) - Platform-specific behavior (macOS `pbcopy`, Linux `xclip`/`wl-copy`, Windows) - pi extensions run in the pi process; clipboard access may have security implications @@ -121,6 +135,7 @@ Either selection model (A or B), but instead of `setEditorText`, write to the sy ## Recommendation: Route B (Visual Mode + setEditorText) **Rationale:** + 1. **No external dependencies** — `setEditorText` is already available 2. **Familiar UX** — vim-style visual mode is intuitive for developers 3. **Contiguous selection** — matches the user's "contiguous lines" description @@ -136,6 +151,7 @@ Either selection model (A or B), but instead of `setEditorText`, write to the sy ## Outstanding Question for User How should the copied text be placed in the editor? + - **Append** to current editor content (preserves what's there) - **Replace** current editor content (clean slate) diff --git a/backlog/docs/doc-6 - Programmatic-Access-to-Production-Logs.md b/backlog/docs/doc-6 - Programmatic-Access-to-Production-Logs.md index 3e4bea4e..58f974f3 100644 --- a/backlog/docs/doc-6 - Programmatic-Access-to-Production-Logs.md +++ b/backlog/docs/doc-6 - Programmatic-Access-to-Production-Logs.md @@ -2,9 +2,10 @@ id: doc-6 title: Programmatic Access to Production Logs type: other -created_date: '2026-05-04 06:43' -updated_date: '2026-05-04 06:55' +created_date: "2026-05-04 06:43" +updated_date: "2026-05-04 06:55" --- + # Implementation Route Analysis: Programmatic Access to Production Logs ## Decision: Route B — Pi Tool via `pi.registerTool()` ✓ SELECTED @@ -28,19 +29,23 @@ The LLM needs a tool it can call to retrieve production logs directly into its c ## Route A: Tidewave MCP Tool (Elixir-side) ### Description + Add a new MCP tool `get_production_logs` to Tidewave that calls the Coolify API from Elixir using `Req`. ### Pros + - Architecturally consistent with existing MCP tools - Req is battle-tested in this codebase - Accessible by any MCP client ### Cons + - Must forward Coolify env vars to Phoenix app runtime - Rewrites the API call in Elixir (no code reuse from existing `fetchLogs`) - New Elixir module to maintain ### Verdict + **Rejected** in favor of Route B. The pi tool approach is simpler, reuses existing code and credentials directly, and keeps the change contained to a single file. --- @@ -48,9 +53,11 @@ Add a new MCP tool `get_production_logs` to Tidewave that calls the Coolify API ## Route B: Pi Tool via `pi.registerTool()` ✓ SELECTED ### Description + Add a `fetch_production_logs` tool registration to the existing `prod-logs` extension. The tool is called by the LLM directly (just like `read`, `bash`, etc.), fetches logs via the existing `fetchLogs()` function, and returns text content to the LLM. ### Pros + - **Direct code reuse**: Same `fetchLogs()` function, same `resolveVar()` helper, same env vars - **No infrastructure changes**: No new Elixir modules, no credential forwarding, no config changes - **First-class LLM tool**: The LLM calls it natively during its agent loop, the result flows through the same context pipeline as any other tool result @@ -58,10 +65,12 @@ Add a `fetch_production_logs` tool registration to the existing `prod-logs` exte - **No external dependencies**: `typebox` is already available as a pi built-in import ### Cons + - pi-only (not accessible from other MCP clients — irrelevant for this use case) - Tool definition consumes a small amount of LLM context window space (same as any tool) ### Architecture Impact + - **Modified file**: `.pi/extensions/prod-logs/index.ts` (add tool registration, ~50 lines) - **No changes**: No Elixir modules, no config, no schemas, no routes, no UI diff --git a/backlog/docs/doc-7 - Pi-access-to-production-errors.md b/backlog/docs/doc-7 - Pi-access-to-production-errors.md index cd2184e5..ef3965df 100644 --- a/backlog/docs/doc-7 - Pi-access-to-production-errors.md +++ b/backlog/docs/doc-7 - Pi-access-to-production-errors.md @@ -2,9 +2,10 @@ id: doc-7 title: Pi access to production errors type: other -created_date: '2026-05-04 08:07' -updated_date: '2026-05-04 08:13' +created_date: "2026-05-04 08:07" +updated_date: "2026-05-04 08:13" --- + # Implementation Routes: pi access to production errors ## Problem @@ -12,6 +13,7 @@ updated_date: '2026-05-04 08:13' The project captures production errors via the `error_tracker` Elixir dependency (stored in `MusicLibrary.TelemetryRepo` SQLite database). Errors can currently only be viewed through the built-in web dashboard at `/dev/errors` (behind login auth, only with `:monitoring_routes` enabled). There is no programmatic access — no API, no tooling, no pi extension. The goal is to enable pi (the coding agent) to fetch and browse production errors. This requires three layers: + 1. A programmatic API to expose error data (behind auth) 2. Pi tools the LLM can call to fetch errors 3. A pi extension for interactive browsing @@ -22,6 +24,7 @@ The goal is to enable pi (the coding agent) to fetch and browse production error **How it works:** Add a new API controller (`MusicLibraryWeb.ErrorsController`) with two endpoints under `/api/v1/errors`: + - `GET /api/v1/errors` — list errors with optional filtering (status, muted, search) and pagination - `GET /api/v1/errors/:id` — single error detail with its occurrences and stacktraces @@ -30,6 +33,7 @@ The controller queries the `error_tracker_errors` and `error_tracker_occurrences Pi tools (`fetch_production_errors` and `fetch_production_error`) make HTTP requests to this API using `fetch()` or `pi.exec("curl", ...)`. The pi extension builds on these tools for an interactive TUI. **Pros:** + - Follows existing patterns exactly (see `CollectionController`, `require_api_token`) - Clean separation of concerns: API layer, tool layer, extension layer - Pi tools work remotely — no need for SSH or filesystem access to the production server @@ -40,6 +44,7 @@ Pi tools (`fetch_production_errors` and `fetch_production_error`) make HTTP requ - The TelemetryRepo already exists and has the tables — no new database work **Cons:** + - Requires a server code change and deployment - Adds two new routes @@ -62,11 +67,13 @@ Pi tools execute SQL queries directly against the TelemetryRepo SQLite database No server-side API changes needed. The pi tools would use `pi.exec("sqlite3", ...)` or read the database file directly. **Pros:** + - Zero server code changes - Immediate access to ALL data — no API shape limitations - Can run complex ad-hoc queries without API changes **Cons:** + - **Production access requires SSH or filesystem access** — violates the project's existing API-based pattern for pi access (cf. `fetch_production_logs` which uses Coolify API, not SSH) - No auth layer — tools have full read access to the entire database - Tightly couples pi tools to the error_tracker schema — any migration could break tools @@ -86,10 +93,12 @@ The direct-access approach introduces deployment friction (SSH key management, f Use the existing Tidewave MCP tools (`tidewave_execute_sql_query`) to query the error_tracker tables directly. This is available in dev but would need the Tidewave MCP server to be accessible in production (e.g., via SSH tunnel or a production-side MCP server). **Pros:** + - No new code at all — uses what's already there - `tidewave_execute_sql_query` already understands the repo structure **Cons:** + - **Tidewave MCP server does not run in production** — it's a development-only tool - Even if it did, running a dev tool against production is architecturally wrong - No browsing UX, no filtering, no pagination — basic SQL results only @@ -106,9 +115,11 @@ Tidewave is a development tool. Exposing it to production would be an architectu Add an endpoint to the existing Coolify-like API pattern used by `fetch_production_logs`. This would require Coolify to expose error_tracker data, which it doesn't natively support. Could potentially parse production logs for error patterns, but that's unstructured and duplicative. **Pros:** + - Consistent with the existing `fetch_production_logs` pattern **Cons:** + - Coolify doesn't expose error_tracker data - ErrorTracker already has structured data — parsing it from raw logs is backwards and lossy - Would require Coolify API changes or custom Coolify plugin diff --git a/backlog/ml-142/mockups.html b/backlog/ml-142/mockups.html index 2c29ccf7..c04549fb 100644 --- a/backlog/ml-142/mockups.html +++ b/backlog/ml-142/mockups.html @@ -51,8 +51,14 @@ overflow: hidden; font-size: 0.82rem; color: var(--text); - font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; - box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06); + font-family: + ui-sans-serif, + system-ui, + -apple-system, + sans-serif; + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.04), + 0 8px 24px rgba(0, 0, 0, 0.06); } .ml-sheet-header { padding: 0.9rem 1rem; @@ -66,9 +72,23 @@ gap: 0.75rem; margin-bottom: 0.75rem; } - .ml-title { font-weight: 600; font-size: 0.98rem; color: var(--text); line-height: 1.2; } - .ml-subtitle { color: var(--text-muted); font-size: 0.75rem; margin-top: 0.15rem; } - .ml-actions { display: flex; align-items: center; gap: 0.4rem; flex-wrap: nowrap; } + .ml-title { + font-weight: 600; + font-size: 0.98rem; + color: var(--text); + line-height: 1.2; + } + .ml-subtitle { + color: var(--text-muted); + font-size: 0.75rem; + margin-top: 0.15rem; + } + .ml-actions { + display: flex; + align-items: center; + gap: 0.4rem; + flex-wrap: nowrap; + } .ml-picker { display: inline-flex; @@ -83,10 +103,22 @@ cursor: default; white-space: nowrap; } - .ml-picker svg { color: var(--text-muted); flex-shrink: 0; } - .ml-picker-label { color: var(--text-muted); font-size: 0.7rem; } - .ml-picker-value { font-weight: 500; font-variant-numeric: tabular-nums; } - .ml-picker-caret { color: var(--text-subtle); margin-left: 0.1rem; } + .ml-picker svg { + color: var(--text-muted); + flex-shrink: 0; + } + .ml-picker-label { + color: var(--text-muted); + font-size: 0.7rem; + } + .ml-picker-value { + font-weight: 500; + font-variant-numeric: tabular-nums; + } + .ml-picker-caret { + color: var(--text-subtle); + margin-left: 0.1rem; + } .ml-btn { display: inline-flex; @@ -102,15 +134,42 @@ white-space: nowrap; font-family: inherit; } - .ml-btn svg { width: 14px; height: 14px; } - .ml-btn-solid { background: var(--primary); color: white; border-color: var(--primary); } - .ml-btn-soft { background: var(--primary-soft-bg); color: var(--primary-soft-text); border-color: transparent; } - .ml-btn-outline{ background: var(--surface); color: var(--text); border-color: var(--border-c); } - .ml-btn-ghost { background: transparent; color: var(--text-muted); } - .ml-btn.icon-only { padding: 0; width: 30px; height: 30px; } - .ml-btn.disabled { opacity: 0.45; } + .ml-btn svg { + width: 14px; + height: 14px; + } + .ml-btn-solid { + background: var(--primary); + color: white; + border-color: var(--primary); + } + .ml-btn-soft { + background: var(--primary-soft-bg); + color: var(--primary-soft-text); + border-color: transparent; + } + .ml-btn-outline { + background: var(--surface); + color: var(--text); + border-color: var(--border-c); + } + .ml-btn-ghost { + background: transparent; + color: var(--text-muted); + } + .ml-btn.icon-only { + padding: 0; + width: 30px; + height: 30px; + } + .ml-btn.disabled { + opacity: 0.45; + } - .ml-btn-group { display: inline-flex; gap: 0.35rem; } + .ml-btn-group { + display: inline-flex; + gap: 0.35rem; + } /* Medium header */ .ml-medium-header { @@ -122,7 +181,13 @@ background: var(--surface-muted); border-bottom: 1px solid var(--border-c); } - .ml-medium-title { display: flex; align-items: center; gap: 0.55rem; font-weight: 600; font-size: 0.82rem; } + .ml-medium-title { + display: flex; + align-items: center; + gap: 0.55rem; + font-weight: 600; + font-size: 0.82rem; + } .ml-badge { display: inline-flex; align-items: center; @@ -140,7 +205,8 @@ /* Checkbox */ .ml-chk { appearance: none; - width: 15px; height: 15px; + width: 15px; + height: 15px; border: 1.5px solid var(--z-400); border-radius: 4px; position: relative; @@ -149,12 +215,17 @@ background: var(--surface); flex-shrink: 0; } - .ml-chk.checked { background: var(--primary); border-color: var(--primary); } + .ml-chk.checked { + background: var(--primary); + border-color: var(--primary); + } .ml-chk.checked::after { - content: ''; + content: ""; position: absolute; - left: 3px; top: 0px; - width: 5px; height: 9px; + left: 3px; + top: 0px; + width: 5px; + height: 9px; border: solid white; border-width: 0 2px 2px 0; transform: rotate(45deg); @@ -170,11 +241,26 @@ align-items: center; font-size: 0.8rem; } - .ml-track:last-child { border-bottom: none; } - .ml-track.selected { background: var(--accent-row); } - .ml-track-num { color: var(--text-subtle); font-size: 0.72rem; text-align: right; font-variant-numeric: tabular-nums; } - .ml-track-title { color: var(--text); } - .ml-track-dur { color: var(--text-muted); font-size: 0.72rem; font-variant-numeric: tabular-nums; } + .ml-track:last-child { + border-bottom: none; + } + .ml-track.selected { + background: var(--accent-row); + } + .ml-track-num { + color: var(--text-subtle); + font-size: 0.72rem; + text-align: right; + font-variant-numeric: tabular-nums; + } + .ml-track-title { + color: var(--text); + } + .ml-track-dur { + color: var(--text-muted); + font-size: 0.72rem; + font-variant-numeric: tabular-nums; + } /* Sticky selection bar */ .ml-sticky { @@ -182,18 +268,33 @@ bottom: 0; background: var(--surface); border-top: 1px solid var(--border-c); - box-shadow: 0 -6px 18px rgba(0,0,0,0.08); + box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.08); padding: 0.65rem 1rem; display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; } - .ml-sticky-info { display: flex; flex-direction: column; line-height: 1.2; } - .ml-sticky-count { font-size: 0.82rem; font-weight: 600; color: var(--text); } - .ml-sticky-meta { font-size: 0.7rem; color: var(--text-muted); } + .ml-sticky-info { + display: flex; + flex-direction: column; + line-height: 1.2; + } + .ml-sticky-count { + font-size: 0.82rem; + font-weight: 600; + color: var(--text); + } + .ml-sticky-meta { + font-size: 0.7rem; + color: var(--text-muted); + } - .ml-body { max-height: 520px; overflow-y: auto; scrollbar-width: thin; } + .ml-body { + max-height: 520px; + overflow-y: auto; + scrollbar-width: thin; + } /* Viewport frames */ .viewport-frame { @@ -208,9 +309,13 @@ text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; - display: flex; justify-content: space-between; + display: flex; + justify-content: space-between; + } + .viewport-label .dim { + color: var(--text-subtle); + font-variant-numeric: tabular-nums; } - .viewport-label .dim { color: var(--text-subtle); font-variant-numeric: tabular-nums; } .grid-two { display: grid; @@ -219,7 +324,9 @@ align-items: start; } @media (max-width: 1000px) { - .grid-two { grid-template-columns: 1fr; } + .grid-two { + grid-template-columns: 1fr; + } } .annotation { @@ -231,7 +338,9 @@ margin-top: 0.75rem; border-radius: 4px; } - .annotation strong { font-weight: 600; } + .annotation strong { + font-weight: 600; + } .hierarchy-diagram { background: var(--surface); @@ -241,56 +350,113 @@ margin-top: 1rem; font-size: 0.82rem; } - .h-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.4rem 0; } - .h-label { font-weight: 600; width: 130px; color: var(--text); } - .h-arrow { color: var(--text-subtle); } - .h-scope { color: var(--text-muted); } + .h-row { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.4rem 0; + } + .h-label { + font-weight: 600; + width: 130px; + color: var(--text); + } + .h-arrow { + color: var(--text-subtle); + } + .h-scope { + color: var(--text-muted); + } /* Mobile-specific overrides on narrow viewport mock */ - .m-narrow .ml-title-row { flex-direction: column; align-items: stretch; } - .m-narrow .ml-title-row .ml-actions-top { justify-content: space-between; } - .m-narrow .ml-picker { flex: 1; justify-content: flex-start; } + .m-narrow .ml-title-row { + flex-direction: column; + align-items: stretch; + } + .m-narrow .ml-title-row .ml-actions-top { + justify-content: space-between; + } + .m-narrow .ml-picker { + flex: 1; + justify-content: flex-start; + } .m-narrow .ml-btn-row { display: flex; gap: 0.4rem; margin-top: 0.5rem; } - .m-narrow .ml-btn-row .ml-picker { flex: 1; } - .m-narrow .ml-btn-row .ml-btn-solid { flex: 0 0 auto; } + .m-narrow .ml-btn-row .ml-picker { + flex: 1; + } + .m-narrow .ml-btn-row .ml-btn-solid { + flex: 0 0 auto; + }

Option A — v2, app-styled + mobile layout + explicit "Scrobble release"

-

Addresses the three iteration asks: using the app's zinc/blue palette and Fluxon button variants, dedicated mobile layout, and a clear top-level "Scrobble release" action.

+

+ Addresses the three iteration asks: using the app's zinc/blue palette and + Fluxon button variants, dedicated mobile layout, and a clear top-level + "Scrobble release" action. +

-
Action hierarchy
+
+ Action hierarchy +
Header action - Scrobble release + + + + Scrobble release all tracks, one submission
Per-medium action - Scrobble disc + + + + Scrobble disc tracks of that medium
Selection action - Scrobble selected - only ticked rows — appears only when something is ticked + + + + Scrobble selected + only ticked rows — appears only when something is ticked
-
- +
Desktop · sheet width ~520px - Fluxon variants: solid primary / soft primary / outline + Fluxon variants: solid primary / soft primary / outline
@@ -302,17 +468,39 @@
- + + + Finished at Now · Apr 22, 14:30
@@ -327,11 +515,22 @@
@@ -363,11 +562,22 @@
@@ -397,7 +607,9 @@ across 2 discs · 10:45 @@ -406,7 +618,10 @@
- Scrobble release sits in the header beside the "Finished at" picker. Solid primary to mark the default action. Per-medium Scrobble disc uses the soft variant to stay a visible shortcut without competing with the primary. + Scrobble release sits in the header beside the "Finished + at" picker. Solid primary to mark the default action. Per-medium + Scrobble disc uses the soft variant to stay a visible + shortcut without competing with the primary.
@@ -416,99 +631,201 @@ Mobile · 360px width iPhone SE class -
-
-
+
+
+
-
+
-
Kid A Mnesia
+
+ Kid A Mnesia +
Radiohead · 2 CDs
- - - + + + + + Finished at - Now + Now -
-
-
-
+
+
+
Disc 1 CD
-
-
+
1 How to Disappear Completely 5:56
-
+
2 The National Anthem 5:51
-
-
+
+
Disc 2 CD
-
-
+
1 Pyramid Song 4:49
-
+
- 2 selected + 2 selected 10:45
-
@@ -517,18 +834,46 @@
- Mobile reflow: title row stacks title + ⋯; the picker and "Release" button share a full-width row (picker grows, button hugs). Per-medium scrobble becomes icon-only to fit. Sticky bar label shortens to "Scrobble" + count + duration. + Mobile reflow: title row stacks title + ⋯; the picker and + "Release" button share a full-width row (picker grows, button hugs). + Per-medium scrobble becomes icon-only to fit. Sticky bar label shortens to + "Scrobble" + count + duration.
-
- Design language notes
-
    -
  • Surfaces use the zinc palette (zinc-50 / zinc-200 / zinc-800) seen across the app.
  • -
  • Buttons use Fluxon variants: solid primary (release + selection), soft primary (per-medium shortcut), outline (menu & picker).
  • -
  • Icons are hero-style (outline-filled), matching existing `hero-play`, `hero-ellipsis-vertical`, `hero-clock`.
  • -
  • Selected rows use a subtle blue-tinted background — matches the dimmed-accent pattern already in the Collection list.
  • -
  • Dark mode swaps to zinc-900/800/700 and a lighter blue-400 primary; the mockup honours `prefers-color-scheme`.
  • +
    + Design language notes
    +
      +
    • + Surfaces use the zinc palette (zinc-50 / zinc-200 / zinc-800) seen across + the app. +
    • +
    • + Buttons use Fluxon variants: solid primary (release + selection), soft + primary (per-medium shortcut), outline (menu & picker). +
    • +
    • + Icons are hero-style (outline-filled), matching existing `hero-play`, + `hero-ellipsis-vertical`, `hero-clock`. +
    • +
    • + Selected rows use a subtle blue-tinted background — matches the + dimmed-accent pattern already in the Collection list. +
    • +
    • + Dark mode swaps to zinc-900/800/700 and a lighter blue-400 primary; the + mockup honours `prefers-color-scheme`. +
    diff --git a/backlog/ml-142/plan.md b/backlog/ml-142/plan.md index c98a83c4..6091a235 100644 --- a/backlog/ml-142/plan.md +++ b/backlog/ml-142/plan.md @@ -6,7 +6,7 @@ The Release sheet's scrobble controls have three concrete friction points, confi 1. **Navigation.** On a multi-medium release, the only way to scrobble a cross-medium selection is the top-level button — users have to scroll all the way back up after picking tracks on disc 3. 2. **No picker for `finished_at`.** The timestamp is always `DateTime.utc_now()`; there is no UI to backfill a listening session. `ScrobbleActivity.scrobble_release/3`, `scrobble_medium/4`, and `scrobble_tracks/4` already accept a `DateTime`, so this is purely a UI gap. -3. **Disabled buttons aren't visually distinct.** All scrobble buttons use `variant="soft"` with only `opacity-70` for disabled — hard to tell state apart. The per-medium button is *always* disabled when any track is selected anywhere, which is surprising. +3. **Disabled buttons aren't visually distinct.** All scrobble buttons use `variant="soft"` with only `opacity-70` for disabled — hard to tell state apart. The per-medium button is _always_ disabled when any track is selected anywhere, which is surprising. A fourth issue surfaced in review: the **print** icon is omnipresent (release + every medium) but rarely used, taking up valuable primary-action space. @@ -118,13 +118,13 @@ A **Reset to now** affordance: if Fluxon's picker exposes a clear action (likely ### 5. Event handlers — summary of changes -| Event | Before | After | -|---|---|---| -| `scrobble_release` | `DateTime.utc_now()` hardcoded | `@finished_at || DateTime.utc_now()` | -| `scrobble_medium` | disabled when selection present; always `utc_now()` | always enabled; uses `@finished_at || utc_now()` | -| `scrobble_selected_tracks` | `DateTime.utc_now()` hardcoded | `@finished_at || DateTime.utc_now()` | -| `clear_finished_at` | — | new; resets form field + assign | -| `validate` | parses `selected_tracks` + `toggle_medium` | also parses `finished_at` | +| Event | Before | After | +| -------------------------- | --------------------------------------------------- | ---------------------------------- | --- | ------------------- | +| `scrobble_release` | `DateTime.utc_now()` hardcoded | `@finished_at | | DateTime.utc_now()` | +| `scrobble_medium` | disabled when selection present; always `utc_now()` | always enabled; uses `@finished_at | | utc_now()` | +| `scrobble_selected_tracks` | `DateTime.utc_now()` hardcoded | `@finished_at | | DateTime.utc_now()` | +| `clear_finished_at` | — | new; resets form field + assign | +| `validate` | parses `selected_tracks` + `toggle_medium` | also parses `finished_at` | The `already_scrobbled` 3-second lockout stays as-is. diff --git a/backlog/ml-143/mockups.html b/backlog/ml-143/mockups.html index a0eeecf3..c1eee971 100644 --- a/backlog/ml-143/mockups.html +++ b/backlog/ml-143/mockups.html @@ -9,11 +9,23 @@ tailwind.config = { darkMode: "class" }; @@ -23,7 +35,8 @@

    Add Record — Cart UI Mockups

    - Three directions for a multi-record import flow. Static HTML, not wired up. + Three directions for a multi-record import flow. Static HTML, not + wired up.

-