Backlog cleanup
This commit is contained in:
-233
@@ -1,233 +0,0 @@
|
||||
---
|
||||
id: ML-169
|
||||
title: "Comprehensive application audit: functional correctness and performance"
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: "2026-05-08 08:46"
|
||||
updated_date: "2026-05-19 08:50"
|
||||
labels:
|
||||
- audit
|
||||
dependencies: []
|
||||
references:
|
||||
- docs/architecture.md
|
||||
- docs/project-conventions.md
|
||||
- >-
|
||||
backlog/tasks/ml-168 -
|
||||
Update-wishlist-index-and-collection-index-when-background-import-finishes.md
|
||||
priority: medium
|
||||
ordinal: 1000
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
Large-scale audit of the application covering two dimensions:
|
||||
|
||||
**1. Functional Issues**: Unhandled async messages from LiveComponents, stale PubSub subscriptions, unexpected state changes from concurrent operations (background workers updating records while user is viewing them).
|
||||
|
||||
**2. Performance Low-Hanging Fruit**: Unnecessary server round trips, missing opportunities for optimistic updates, redundant data reloads, and synchronous blocking work in mount.
|
||||
|
||||
The audit is divided into 4 independent phases that can be tackled in parallel or sequentially. Each phase produces findings that feed into fix tasks. Full implementation plan in the task plan section.
|
||||
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [x] #1 Phase 1 (async messages): all LiveComponents that send messages to parent LiveViews have matching handle_info clauses documented; missing handlers identified with severity
|
||||
- [x] #2 Phase 2 (PubSub lifecycle): all subscribe/unsubscribe pairs verified correct; stale subscription risks identified; ML-168 gap separately accounted for
|
||||
- [x] #3 Phase 3 (state change safety): concurrent record-mutation patterns audited; {:update, record} guards and manage_subscription patterns verified across all navigation paths
|
||||
- [x] #4 Phase 4 (performance): unnecessary round trips, redundant full-stream reloads, and synchronous mount work identified with estimated impact and fix approach
|
||||
- [x] #5 Each phase produces a written finding report with file paths, line references, severity ratings, and recommended fixes
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
|
||||
# Audit Implementation Plan
|
||||
|
||||
## Audit Scope
|
||||
|
||||
This audit covers all 14 LiveViews, all LiveComponents that send messages to parents, all PubSub subscription points, and all Oban worker-LiveView interaction patterns. The full architecture is documented in `docs/architecture.md`.
|
||||
|
||||
## Phase Structure
|
||||
|
||||
Each phase is a standalone investigation that produces a findings document. Fixes are NOT implemented in these phases — only identification, severity rating, and fix recommendations.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Async Message Audit (LiveComponent → Parent)
|
||||
|
||||
**Goal**: Verify every LiveComponent message has a matching `handle_info` in its parent LiveView.
|
||||
|
||||
**Known message producers and their parents**:
|
||||
|
||||
| Component | Messages sent | Consumer LiveViews | Status |
|
||||
| ------------------------------ | -------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `RecordForm` | `{:saved, record}` | CollectionLive.Index, WishlistLive.Index, CollectionLive.Show, WishlistLive.Show | ✅ All four have handlers |
|
||||
| `AddRecord` | `{:imported_single, record}`, `{:imported_async, count}` | CollectionLive.Index, WishlistLive.Index | ✅ Both have handlers |
|
||||
| `Chat` | `:chats_changed` | CollectionLive.Index, CollectionLive.Show, WishlistLive.Show, ArtistLive.Show | ✅ All four have handlers |
|
||||
| `Release` | `{tag, release}` (dynamic tag) | ScrobbleLive.ReleaseShow, CollectionLive.Show | ⚠️ CollectionLive.Show renders Release but has NO `handle_info({:release_loaded, _})` — needs investigation |
|
||||
| `ArtistLive.Form` | `{:saved, artist_info}` | ArtistLive.Show | ✅ Has handler |
|
||||
| `RecordSetLive.Form` | `{:created, _}`, `{:updated, _}` | RecordSetLive.Index | ✅ Has handlers |
|
||||
| `RecordSetLive.RecordPicker` | `{:added, record_set}` | RecordSetLive.Index | ✅ Has handler |
|
||||
| `ScrobbleRulesLive.Form` | `{:created, _}`, `{:updated, _}` | ScrobbleRulesLive.Index | ✅ Has handlers |
|
||||
| `ScrobbledTracksLive.Form` | `{:saved, _track}` | ScrobbledTracksLive.Index | ✅ Has handler |
|
||||
| `OnlineStoreTemplateLive.Form` | `{:saved, template}` | OnlineStoreTemplateLive.Index | ✅ Has handler |
|
||||
| `ScrobbleRulePicker` | `{:rule_created, _rule}` | ScrobbledTracksLive.Index, StatsLive.Index | ✅ Both have handlers |
|
||||
|
||||
**Investigation steps**:
|
||||
|
||||
1. Cross-reference every `send(self(), ...)` call site (found via `grep -rn "send(self()"`) with `handle_info` clauses in the parent LiveView
|
||||
2. Specifically trace the `Release` component usage in `CollectionLive.Show` — it renders `Release` with `on_release_loaded={:release_loaded}`, but `CollectionLive.Show` has no `handle_info({:release_loaded, _})` clause. Determine if this is a dead message or a missing handler
|
||||
3. Verify no component sends messages when rendered in a context that doesn't handle them (e.g., modal closed, wrong live_action)
|
||||
4. Look for `send/2` calls using patterns NOT matching `{__MODULE__, _}`
|
||||
|
||||
**Files to check**: All `lib/music_library_web/components/*.ex`, all `lib/music_library_web/live/**/*.ex`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: PubSub Subscription Lifecycle Audit
|
||||
|
||||
**Goal**: Verify all PubSub subscriptions are properly paired with unsubscriptions; identify stale subscription risks.
|
||||
|
||||
**Subscription inventory**:
|
||||
|
||||
| Subscribe | Topic | Where | Unsubscribe | Risk |
|
||||
| ---------------------------- | -------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| `Records.subscribe(id)` | `"records:#{id}"` | CollectionLive.Show, WishlistLive.Show (via `manage_subscription/2`) | ✅ `manage_subscription/2` unsubscribes old ID before subscribing new — called on every `handle_params` | Low |
|
||||
| `ListeningStats.subscribe()` | `"listening_stats:update"` | StatsLive.Index (`mount`), ScrobbledTracksLive.Index (`mount`) | ⚠️ Never unsubscribed. Only called when connected. | Medium — PID monitoring handles cleanup but double-subscription on reconnect possible |
|
||||
|
||||
**Broadcast inventory**:
|
||||
|
||||
| Broadcast | Topic | Message | Called from |
|
||||
| ----------------------------------- | -------------------------- | ------------------- | ----------------------------------------- |
|
||||
| `Records.notify_update/1` | `"records:#{id}"` | `{:update, record}` | Workers (needs audit — see Phase 3) |
|
||||
| `ListeningStats.broadcast_update/1` | `"listening_stats:update"` | `%{track_count: n}` | `lib/music_library/listening_stats.ex:68` |
|
||||
|
||||
**Investigation steps**:
|
||||
|
||||
1. Verify `manage_subscription/2` is called on ALL navigation paths: browser back/forward, direct URL, push_navigate, push_patch
|
||||
2. Check that `Records.subscribe/1` is NEVER called outside of `manage_subscription/2` (only two Show LiveViews should call it)
|
||||
3. Verify `ListeningStats.subscribe()` doesn't double-subscribe on LiveView reconnect (check if Phoenix.LiveView re-mounts fully or reuses process)
|
||||
4. Check that `Records.unsubscribe/1` is called on terminate — currently it's only called on navigation, not process termination. Phoenix.PubSub auto-cleans when PID dies, but verify
|
||||
5. Audit `Records.notify_update/1` broadcast sites against intended behavior (see Phase 3)
|
||||
|
||||
**Files to check**: `lib/music_library/records.ex`, `lib/music_library/listening_stats.ex`, `lib/music_library_web/live_helpers/record_actions.ex`, all Show LiveViews
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Concurrent State Change Safety Audit
|
||||
|
||||
**Goal**: Verify that background worker updates to records don't corrupt LiveView state.
|
||||
|
||||
**Pattern**: Oban worker modifies record → calls `Records.notify_update/1` → PubSub broadcasts `{:update, record}` → Show LiveView's `handle_info({:update, record}, socket)` applies update via `RecordActions.handle_record_updated/2`.
|
||||
|
||||
**Guard**: `record.id == socket.assigns.record.id` in both CollectionLive.Show and WishlistLive.Show ✅
|
||||
|
||||
**Worker-to-broadcast audit** (every worker that modifies records):
|
||||
|
||||
| Worker | Modifies record? | Calls notify_update? | Check |
|
||||
| ----------------------------------- | ----------------------------------- | -------------------- | ------------------------ |
|
||||
| `RefreshCover` | Yes — updates cover_url, cover_hash | Need to verify | Read worker source |
|
||||
| `PopulateGenres` | Yes — updates genres | Need to verify | Read worker source |
|
||||
| `GenerateRecordEmbedding` | Yes (indirect via Similarity) | Need to verify | Read worker + Similarity |
|
||||
| `RecordRefreshMusicBrainzData` | Yes — updates musicbrainz_data | Need to verify | Read worker source |
|
||||
| `RecordRefreshAllMusicBrainzData` | Yes (batch) | Need to verify | Read worker + Batch |
|
||||
| `RecordGenerateAllEmbeddings` | Yes (batch) | Need to verify | Read worker + Batch |
|
||||
| `ImportFromMusicbrainzRelease` | Creates new record | Need to verify | Read worker source |
|
||||
| `ImportFromMusicbrainzReleaseGroup` | Creates new records | Need to verify | Read worker source |
|
||||
| `PruneAssets` | No (assets only) | N/A | Skip |
|
||||
| `PruneAssetCache` | No (cache only) | N/A | Skip |
|
||||
| `ApplyScrobbleRules` | No (scrobble data) | N/A | Skip |
|
||||
| `FetchArtistInfo` | No (artist info) | N/A | Skip |
|
||||
| All `ArtistRefresh*` workers | No (artist info) | N/A | Skip |
|
||||
| `RefreshScrobbles` | No (scrobble data) | N/A | Skip |
|
||||
| `BackfillScrobbledTracks` | No (scrobble data) | N/A | Skip |
|
||||
| `SendRecordsOnThisDayEmail` | No (email) | N/A | Skip |
|
||||
| `RepoVacuum` / `RepoOptimize` | No (maintenance) | N/A | Skip |
|
||||
|
||||
**Additional checks**:
|
||||
|
||||
1. **Double-update race**: If two workers update the same record simultaneously, does the LiveView handle both `handle_info` calls correctly? (Each re-assigns `:record` — second update is latest, which is correct since workers touch different fields)
|
||||
2. **Form edit + background update race**: User editing in `RecordForm` modal while a background worker updates the record. The form holds a stale `@record` assign. When the worker broadcasts `{:update, record}`, the Show page updates `socket.assigns.record`, but the form LiveComponent has its OWN `@record` assign set during `update_many`. What happens? This is a potential functional bug.
|
||||
3. **ArtistLive.Show**: Does NOT handle `{:update, record}` — artist updates use a different mechanism (artist info is updated through `ArtistLive.Form` which sends `{:saved, artist_info}` directly). Verify this is intentional and complete.
|
||||
|
||||
**Files to check**: All `lib/music_library/worker/*.ex`, `lib/music_library/records.ex` (notify_update), `lib/music_library/records/similarity.ex`, all Show LiveViews, `RecordForm` component
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Performance Low-Hanging Fruit Audit
|
||||
|
||||
**Goal**: Identify unnecessary work — DB queries, stream resets, server round trips — that could be eliminated or deferred.
|
||||
|
||||
### 4a. Unnecessary Full-Stream Reloads
|
||||
|
||||
| LiveView | Stream name | Reset trigger | Optimizable? | Impact |
|
||||
| ----------------------------- | ---------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------- |
|
||||
| CollectionLive.Index | `:records` | Every param change, form save, display toggle | Display toggle (grid↔list) could be CSS class only — same data, different render | Medium: 1 FTS query per toggle |
|
||||
| WishlistLive.Index | `:records` | Same as above | Same | Medium |
|
||||
| ScrobbleRulesLive.Index | `:scrobble_rules` | Form save: does `stream_insert` then `load_and_assign_rules` (reset) | The `stream_insert` is immediately overwritten by full reload — redundant | Low: cosmetic |
|
||||
| OnlineStoreTemplateLive.Index | `:templates` | Form save: same redundant `stream_insert` + full reload | Same | Low |
|
||||
| ScrobbledTracksLive.Index | `:tracks` | Form save, PubSub scrobble update | Could incrementally `stream_insert` the edited track instead of full reload | Medium: full FTS on every scrobble batch |
|
||||
| RecordSetLive.Index | `:record_sets` | Form create | Could `stream_insert` the new set without full reload | Low |
|
||||
| StatsLive.Index | `:recent_tracks`, `:recent_albums` | PubSub scrobble update | Incremental `stream_insert` could replace full reset | Medium: resets on every scrobble batch |
|
||||
|
||||
### 4b. Synchronous Mount Work → Async Candidates
|
||||
|
||||
| LiveView | Blocking queries | Recommendation | Impact |
|
||||
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ----------------------------- |
|
||||
| StatsLive.Index | `count_records_by_artist(20)`, `count_records_by_genre(20)`, `count_records_by_release_year(20)`, `get_records_on_this_day()`, `get_latest_record()` | Move to `assign_async` — these are below-the-fold charts | High: 5 queries blocking TTFB |
|
||||
| CollectionLive.Index | `Chats.count_chats(:collection, ...)` | Small, acceptable | None |
|
||||
|
||||
### 4c. Server Round Trips → Client-Side Candidates
|
||||
|
||||
| Interaction | Current | Optimizable? | Impact |
|
||||
| ------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------- |
|
||||
| Display mode toggle (grid↔list) | Server `handle_event("set_display")` → full DB query + stream reset | Switch to client-side JS hook that toggles CSS class | Medium: eliminates 1 round trip + 1 DB query |
|
||||
| Search input | Every keystroke triggers `phx-change` → `handle_event("search")` → DB query | Check if `phx-debounce` is set. If not, add 300ms debounce | Medium: reduces query load |
|
||||
| Sort order change | `push_patch` → `handle_params` → DB query | Cannot be client-side (pagination depends on sort order) | N/A |
|
||||
|
||||
### 4d. Optimistic UI Opportunities
|
||||
|
||||
| Operation | Current behavior | Optimistic approach | Risk |
|
||||
| ---------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------ |
|
||||
| Record delete | `Repo.delete` → `stream_delete` (confirmed) | `stream_delete` immediately → `Repo.delete` in background; rollback on error | Low: delete failures are rare |
|
||||
| Form save in modal | `push_patch` to base index → full reload | `stream_insert` updated record into parent stream without navigation | Medium: need to handle re-sort |
|
||||
| Scrobble rule enabled toggle | Full list reload via `push_patch` | Toggle `stream_insert` item in-place | Low |
|
||||
| Online store template toggle | Full list reload via `push_patch` | Toggle `stream_insert` item in-place | Low |
|
||||
|
||||
### 4e. N+1 and Query Pattern Audit
|
||||
|
||||
**Checklist**:
|
||||
|
||||
- [ ] `CollectionLive.Show.handle_params/3` — loads record, last_listened_track, play_count, record_sets, chat_count, embedding_text, similar_records. Check for N+1.
|
||||
- [ ] `ArtistLive.Show.handle_params/3` — loads artist, collection_records, wishlist_records. Check for N+1.
|
||||
- [ ] Stream rendering — check `.record_grid`, `.record_list`, `.record_card` components for per-item DB calls
|
||||
- [ ] Verify preloads cover all rendered fields (artists, formats, genres, dominant_colors)
|
||||
|
||||
**Investigation method**:
|
||||
|
||||
1. Use QueryReporter skill to capture SQL for: `/collection`, `/collection/:id`, `/artists/:id`, `/stats`
|
||||
2. Review stream usage with `grep -rn "stream(" lib/music_library_web/live/`
|
||||
3. Check for `Repo` calls inside LiveComponents (should delegate to context)
|
||||
4. Check for `Repo` calls inside `.heex` templates (should be in `handle_params`)
|
||||
|
||||
**Files to check**: All `lib/music_library_web/live/**/*.ex`, all `lib/music_library_web/components/*.ex`
|
||||
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Definition of Done
|
||||
|
||||
<!-- DOD:BEGIN -->
|
||||
|
||||
- [x] #1 Audit report for each phase written as a Backlog.md document with file paths, line references, severity ratings, and fix recommendations
|
||||
- [x] #2 At least one concrete finding identified per phase before considering the phase complete
|
||||
- [x] #3 Each finding includes a specific file:line reference (not just a general concern)
|
||||
- [x] #4 Findings are triaged by severity: HIGH (crashes/data loss), MEDIUM (wrong behavior/noticeable perf), LOW (cosmetic/redundant)
|
||||
- [x] #5 ML-168 is referenced where relevant but not duplicated — this audit identifies NEW issues only
|
||||
- [x] #6 No fixes implemented during audit — only identification and recommendation
|
||||
<!-- DOD:END -->
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
id: ML-169.1
|
||||
title: "Phase 3: Concurrent state change safety audit"
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: "2026-05-08 08:59"
|
||||
updated_date: "2026-05-19 08:50"
|
||||
labels:
|
||||
- audit
|
||||
- ready
|
||||
dependencies: []
|
||||
references:
|
||||
- lib/music_library/worker/
|
||||
- lib/music_library/records.ex
|
||||
- lib/music_library_web/components/record_form.ex
|
||||
modified_files:
|
||||
- >-
|
||||
docs/audits/phase3-concurrent-state-safety/doc-26 -
|
||||
Audit-Report-Concurrent-State-Change-Safety-Phase-3.md
|
||||
parent_task_id: ML-169
|
||||
priority: medium
|
||||
ordinal: 4000
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
Audit whether background Oban worker record mutations are safely reflected in LiveView state without data corruption or stale UI.
|
||||
|
||||
Key risks to investigate:
|
||||
|
||||
1. Worker-broadcast consistency: do all record-modifying workers call Records.notify_update/1?
|
||||
2. Form edit + background update race: editing in RecordForm modal while worker updates record
|
||||
3. Double-update handling: two workers updating same record simultaneously
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [x] #1 Every record-modifying Oban worker audited for notify_update/1 call after success (RefreshCover, PopulateGenres, GenerateRecordEmbedding, RecordRefreshMusicBrainzData, ImportFromMusicbrainzRelease, ImportFromMusicbrainzReleaseGroup, all RecordAll\* batch workers)
|
||||
- [x] #2 Workers that do NOT modify records confirmed as correctly NOT broadcasting (PruneAssets, ApplyScrobbleRules, all ArtistRefresh\*, etc.)
|
||||
- [x] #3 Form edit + background update race evaluated: trace what happens when RecordForm holds stale @record assign while worker broadcasts {:update, record} to parent Show page
|
||||
- [x] #4 Double-update race evaluated: confirm both handle_info calls apply safely (workers touch different fields, second wins)
|
||||
- [x] #5 ArtistLive.Show confirmed as intentionally not handling {:update, record} (artist updates use ArtistLive.Form → {:saved, artist_info} path)
|
||||
- [x] #6 Findings report written as a Backlog.md document with file:line references, severity ratings, and fix recommendations
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
|
||||
Audit complete. Report written at docs/audits/phase3-concurrent-state-safety/doc-26.
|
||||
|
||||
Key findings:
|
||||
|
||||
- All 8 record-modifying workers correctly broadcast notify_update or broadcast_index_changed
|
||||
- 18 non-record workers confirmed as correctly NOT broadcasting
|
||||
- MEDIUM finding: form edit + background update race — handle_info({:update, record}) overwrites @record during :edit. Recommended fix: add live_action guard to both Show LiveViews.
|
||||
- Double-update race is SAFE: Ecto changesets only UPDATE SET changed fields, SQLite serializes writes
|
||||
- ArtistLive.Show confirmed intentionally not handling {:update, record}
|
||||
<!-- SECTION:NOTES:END -->
|
||||
@@ -1,79 +0,0 @@
|
||||
---
|
||||
id: ML-169.2
|
||||
title: "Phase 4: Performance low-hanging fruit audit"
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: "2026-05-08 08:59"
|
||||
updated_date: "2026-05-19 08:50"
|
||||
labels:
|
||||
- audit
|
||||
- ready
|
||||
dependencies: []
|
||||
references:
|
||||
- lib/music_library_web/live/stats_live/index.ex
|
||||
- lib/music_library_web/live_helpers/index_actions.ex
|
||||
- lib/music_library_web/live_helpers/record_actions.ex
|
||||
- .agents/skills/query-reporter/SKILL.md
|
||||
- /tmp/queries-ml-audit.sql
|
||||
- >-
|
||||
backlog/documents/doc-27 -
|
||||
Phase-4-Performance-Audit-Low-Hanging-Fruit-Findings.md
|
||||
parent_task_id: ML-169
|
||||
priority: medium
|
||||
ordinal: 2000
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
Identify unnecessary DB queries, redundant full-stream reloads, server round trips that could be client-side, missing optimistic UI opportunities, and blocking mount work that could be deferred.
|
||||
|
||||
Use QueryReporter skill to capture SQL traces for key page loads as evidence.
|
||||
|
||||
Key pre-flagged areas:
|
||||
|
||||
- StatsLive.Index: 5 synchronous queries in mount (TTFB impact)
|
||||
- Display toggle (grid/list): full FTS query when CSS-only suffices
|
||||
- ScrobbleRulesLive + OnlineStoreTemplateLive: redundant stream_insert + immediate full reload
|
||||
- ScrobbledTracksLive + StatsLive: full stream reset on every scrobble batch instead of incremental insert
|
||||
- Search inputs: verify phx-debounce is set
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [x] #1 SQL traces captured via QueryReporter for /collection, /collection/:id, /stats, /artists/:id
|
||||
- [x] #2 Stream reset audit complete: every stream(:name, data, reset: true) call site evaluated for incremental-update alternatives
|
||||
- [x] #3 Display toggle (grid/list) evaluated: confirmed whether CSS-only toggle can replace full DB reload
|
||||
- [x] #4 StatsLive.Index mount blocking work quantified: 5 queries identified, assign_async feasibility assessed
|
||||
- [x] #5 Search input debounce verified: phx-debounce attribute presence checked on all search forms
|
||||
- [x] #6 Optimistic UI opportunities identified for delete, form save, toggle operations
|
||||
- [x] #7 N+1 audit complete: handle_params, stream rendering, and LiveComponent Repo calls checked
|
||||
- [x] #8 Findings report written as a Backlog.md document with file:line references, severity ratings, and fix recommendations
|
||||
<!-- AC:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
|
||||
## Audit Complete: 9 findings across 4 severity levels
|
||||
|
||||
**SQL traces captured** via QueryReporter at `/tmp/queries-ml-audit.sql` for all 4 key pages (StatsLive, CollectionLive.Index, CollectionLive.Show, ArtistLive.Show).
|
||||
|
||||
### Key findings:
|
||||
|
||||
**HIGH severity (3)**:
|
||||
|
||||
1. **Display toggle triggers full DB reload** — grid/list toggle in `handle_set_display` re-executes FTS search when CSS-only toggle suffices. Fix: remove `load_and_assign_records` call.
|
||||
2. **StatsLive.Index mount: 10 sync queries** — home page TTFB blocked by 10 sequential queries. `assign_async` already used elsewhere on the same page — extend it to cover heavy queries.
|
||||
3. **Scrobble updates trigger full stream reset** — every 5-minute cron batch causes `reset: true` on all connected clients for both StatsLive and ScrobbledTracksLive.
|
||||
|
||||
**MEDIUM severity (4)**: 4. **Redundant stream_insert + immediate full reload** in ScrobbleRulesLive and OnlineStoreTemplateLive — `stream_insert` is immediately nullified by `load_and_assign_*`. 5. **Full resets on pagination** — `reset: true` on page changes when append could suffice. 6. **Missing optimistic UI** for delete operations — DB waits block UI. 7. **Correlated subqueries** in `ListeningStats.tracks_with_record_info_query` — 3 subqueries per row × 100 tracks.
|
||||
|
||||
**PASS (2)**: 8. All search inputs have `phx-debounce` set. 9. No direct N+1 issues found in LiveView/LiveComponent layer (no direct Repo calls).
|
||||
|
||||
Full report: `docs/backlog/documents/doc-27` — includes file:line references, SQL traces, and fix recommendations for each finding.
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
id: ML-169.3
|
||||
title: "Phase 2: PubSub subscription lifecycle audit"
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: "2026-05-08 08:59"
|
||||
updated_date: "2026-05-19 08:50"
|
||||
labels:
|
||||
- audit
|
||||
- ready
|
||||
dependencies: []
|
||||
references:
|
||||
- lib/music_library/records.ex
|
||||
- lib/music_library/listening_stats.ex
|
||||
- lib/music_library_web/live_helpers/record_actions.ex
|
||||
modified_files:
|
||||
- >-
|
||||
docs/audits/phase2-pubsub-lifecycle/doc-25 -
|
||||
Audit-Report-PubSub-Subscription-Lifecycle-Phase-2.md
|
||||
parent_task_id: ML-169
|
||||
priority: medium
|
||||
ordinal: 5000
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
Audit all PubSub subscribe/unsubscribe pairs for correctness. Verify no stale subscriptions or double-subscription risks.
|
||||
|
||||
Pre-flagged concern: ListeningStats.subscribe() is called in mount by StatsLive.Index and ScrobbledTracksLive.Index but never unsubscribed. Phoenix.PubSub monitors PID for auto-cleanup, but double-subscription on LiveView reconnect is possible and needs verification.
|
||||
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [x] #1 Records.subscribe/1 and Records.unsubscribe/1 call sites audited — confirmed only called via manage_subscription/2 in Show LiveViews
|
||||
- [x] #2 manage_subscription/2 verified correct across all navigation paths (direct URL, browser back/forward, push_navigate, push_patch)
|
||||
- [x] #3 ListeningStats.subscribe() double-subscription risk on reconnect evaluated — either confirmed safe by PID monitoring or flagged with fix
|
||||
- [x] #4 Termination cleanup verified: Phoenix.PubSub auto-cleans on PID death confirmed; any manual unsubscribe gaps documented
|
||||
- [x] #5 Records.notify_update/1 broadcast sites audited against intended behavior (coordinates with Phase 3 worker audit)
|
||||
- [x] #6 Findings report written as a Backlog.md document with file:line references, severity ratings, and fix recommendations
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
|
||||
Audit complete. Report written at docs/audits/phase2-pubsub-lifecycle/doc-25.
|
||||
|
||||
Key findings:
|
||||
|
||||
- 27 call sites across subscribe, unsubscribe, broadcast, and consumer handle_info verified
|
||||
- All 8 subscribe sites have cleanup coverage (explicit unsubscribe or PID-death auto-clean)
|
||||
- Pre-flagged ListeningStats.subscribe() double-subscription risk is a false positive: Phoenix.PubSub MapSet deduplicates per PID, and LiveView reconnect reuses the same PID within grace period
|
||||
- No code changes required
|
||||
<!-- SECTION:NOTES:END -->
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
---
|
||||
id: ML-169.4
|
||||
title: "Phase 1: Async message audit (LiveComponent → Parent handle_info coverage)"
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: "2026-05-08 08:59"
|
||||
updated_date: "2026-05-19 08:50"
|
||||
labels:
|
||||
- audit
|
||||
- ready
|
||||
dependencies: []
|
||||
references:
|
||||
- audits/phase1-async-message-coverage
|
||||
parent_task_id: ML-169
|
||||
priority: medium
|
||||
ordinal: 3000
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
Audit every LiveComponent message producer against its parent LiveView's handle_info clauses. Identify any missing or dead handlers.
|
||||
|
||||
Pre-flagged concern: the Release component renders in CollectionLive.Show with `on_release_loaded={:release_loaded}`, but CollectionLive.Show has no `handle_info({:release_loaded, _})` clause. Needs investigation — is this a dead message or a missing handler?
|
||||
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [x] #1 Every send(self(), ...) call site in lib/music_library_web/components/ matched against handle_info in its consumer LiveViews
|
||||
- [x] #2 Release component's {:release*loaded, *} message traced end-to-end in CollectionLive.Show context — either confirmed harmless or flagged as bug
|
||||
- [x] #3 All 11 message-producing components verified: RecordForm, AddRecord, Chat, Release, ArtistLive.Form, RecordSetLive.Form, RecordSetLive.RecordPicker, ScrobbleRulesLive.Form, ScrobbledTracksLive.Form, OnlineStoreTemplateLive.Form, ScrobbleRulePicker
|
||||
- [x] #4 Any send/2 calls using non-{**MODULE**, \_} patterns found and verified
|
||||
- [x] #5 Findings report written as a Backlog.md document with file:line references, severity ratings, and fix recommendations
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
|
||||
## Audit Execution Notes
|
||||
|
||||
### Method
|
||||
|
||||
- Grepped all `send(self(), ...)` call sites in `lib/music_library_web/components/` and `lib/music_library_web/live/` (excluding test files)
|
||||
- Grepped all `handle_info` clauses in `lib/music_library_web/live/`
|
||||
- Cross-referenced every `send(self())` call site against parent LiveView `handle_info` clauses
|
||||
- Verified component render locations to confirm the consumer LiveView is the correct parent
|
||||
- Verified all module aliases match (e.g., AddRecord = MusicLibraryWeb.Components.AddRecord)
|
||||
|
||||
### Pre-flagged concern: Release component in CollectionLive.Show
|
||||
|
||||
**RESULT: HARMLESS** — The `notify_release_loaded/2` function in release.ex checks `socket.assigns[:on_release_loaded]`. When nil (CollectionLive.Show doesn't set it), the function returns `:ok` without calling `send/2`. No dead message, no missing handler. The Release component is rendered at line 304 in CollectionLive.Show with `show_print?={true}` and `timezone={@timezone}` but no `on_release_loaded` attribute, which is intentional.
|
||||
|
||||
### Key findings
|
||||
|
||||
- 11/11 components fully verified with 0 missing handlers
|
||||
- 3 non-MODULE patterns found, all verified safe
|
||||
- Release component's dynamic tag pattern works but could benefit from type-safe refactoring (low priority)
|
||||
- RecordSetLive.Show correctly only handles `:updated` since it renders Form only in `:edit` mode
|
||||
|
||||
Full report at @audits/phase1-async-message-coverage
|
||||
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
|
||||
## Result: PASS — No missing handlers or dead messages found
|
||||
|
||||
### Coverage
|
||||
|
||||
- **14 `send(self(), ...)` call sites** across 11 components all matched to corresponding `handle_info` clauses
|
||||
- **0 missing handlers**, **0 dead messages**
|
||||
- **3 non-`{__MODULE__, _}` patterns** all verified safe:
|
||||
1. Release component dynamic tag — safe, nil-guarded
|
||||
2. ScrobbleLive.Index self-send — has matching handler
|
||||
3. ShowToast hook — standard LiveView attach_hook pattern
|
||||
|
||||
### Pre-flagged concern resolved
|
||||
|
||||
Release component in CollectionLive.Show: **HARMLESS**. The `notify_release_loaded/2` checks `socket.assigns[:on_release_loaded]` and skips sending when nil. CollectionLive.Show intentionally doesn't set `on_release_loaded`.
|
||||
|
||||
### Full report
|
||||
|
||||
Documented at `audits/phase1-async-message-coverage` with file:line references, severity ratings, and all 11 components verified.
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
-309
@@ -1,309 +0,0 @@
|
||||
---
|
||||
id: ML-187
|
||||
title: Restructure pre-commit hooks to run only necessary checks depending on change
|
||||
status: Done
|
||||
assignee:
|
||||
- pi
|
||||
created_date: "2026-05-15 08:41"
|
||||
updated_date: "2026-05-18 15:09"
|
||||
labels: []
|
||||
dependencies: []
|
||||
modified_files:
|
||||
- scripts/dev/precommit
|
||||
- docs/project-conventions.md
|
||||
- docs/available-tasks.md
|
||||
- AGENTS.md
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
Currently pre-commit hooks run the entire verification suite irrespectively of the change. This is inefficient and tedious. Some examples of annoyances.
|
||||
|
||||
1. Changes to /backlog items only need the pretty linter.
|
||||
2. Changes to documentation only need the pretty linter.
|
||||
3. Changes to the presto application only need to run the (upcoming) presto test suite.
|
||||
4. Possibly some other patterns, need to verify the complete repo structure.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [x] #1 Backlog-only changes trigger only prettier on backlog files
|
||||
- [x] #2 Docs-only changes trigger only prettier on docs files
|
||||
- [x] #3 Elixir changes (without mix.exs/mix.lock) trigger credo, sobelow, gettext, format, and test, but NOT deps.unlock
|
||||
- [x] #4 mix.exs or mix.lock changes trigger deps.unlock in addition to other Elixir checks
|
||||
- [x] #5 Shell script changes trigger only shellcheck
|
||||
- [x] #6 Presto changes trigger only pytest
|
||||
- [x] #7 Dockerfile changes trigger validate-docker-image
|
||||
- [x] #8 Empty STAGED exits early without errors
|
||||
- [x] #9 Combined changes from multiple categories trigger all relevant checks
|
||||
- [x] #10 Documentation updated: project-conventions.md has Pre-commit Hooks section, available-tasks.md has updated description
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
|
||||
## Objective
|
||||
|
||||
Restructure `scripts/dev/precommit` (the pre-commit hook driver) to inspect staged files and run only the checks relevant to the change, instead of always running the full verification suite (credo, sobelow, tests, shellcheck, prettier, etc.).
|
||||
|
||||
### Problem→Solution Mapping
|
||||
|
||||
- **Problem**: Staging a markdown file in `docs/` or `backlog/` triggers `mix test` (~minutes), `mix credo`, `shellcheck`, and all other checks — none relevant to markdown.
|
||||
- **Problem**: Staging `presto/main.py` triggers the full Elixir test suite, but only the presto test suite (`pytest`) is relevant.
|
||||
- **Problem**: Staging `Dockerfile` doesn't trigger `dev:validate-docker-image` — that check runs only in CI, too late.
|
||||
- **Solution**: Categorize staged files by path pattern and gate each check behind `grep -qE` on the staged file list. No new dependencies.
|
||||
|
||||
## Approach: Pattern-based conditional execution in the precommit script
|
||||
|
||||
**Chosen approach**: Modify `scripts/dev/precommit` to read staged file paths (already exported as `$STAGED` by `.git/hooks/pre-commit`) and use `grep -qE` pattern matching to determine which check categories to run.
|
||||
|
||||
**Alternatives considered and rejected**:
|
||||
|
||||
- **Lefthook / pre-commit framework**: Adds a Node.js dependency, requires config file. Overkill. The project already has a working bash-based pre-commit script; we're just adding conditions.
|
||||
- **Separate mise tasks per category**: Would require chaining multiple mise invocations, adds indirection without benefit. The single script approach keeps debugging simple (`bash -x scripts/dev/precommit`).
|
||||
- **Makefile-based**: Unnecessary abstraction layer. Bash with `set -e` is sufficient.
|
||||
- **Git hook as dispatcher to multiple scripts**: The hook already delegates to `mise run dev:precommit`; keeping all logic in one script is simpler than splitting across hook + multiple scripts.
|
||||
|
||||
## File Categories and Their Checks
|
||||
|
||||
| Category | `grep -E` pattern | Checks to run |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **elixir** | `^(lib/\|test/\|config/\|mix\.exs\|mix\.lock\|priv/repo/migrations/\|priv/gettext/\|\.credo\.exs\|\.formatter\.exs\|\.sobelow-conf)` | `mix format --check-formatted`<br>`mix credo --strict`<br>`mix sobelow --compact --exit`<br>`mix gettext.extract --check-up-to-date`<br>`mix deps.unlock --unused` †<br>`mix test` |
|
||||
| **shell** | `^(scripts/\|\.shellcheckrc)` | `fd . 'scripts/' --exclude '*.hurl' -t file --exec shellcheck --color` |
|
||||
| **assets** | `^(assets/\|\.pi/extensions/.*\.(ts\|js\|json)$)` | `prettier --check 'assets/css/**/*.css' 'assets/js/**/*.js' '.pi/extensions/**/*.{ts,js,json}' '!.pi/extensions/**/node_modules/**'` |
|
||||
| **docs** | `^(docs/\|README\.md\|AGENTS\.md)` | `prettier --check 'docs/**/*.md' 'docs/**/*.livemd' 'README.md' 'AGENTS.md'` |
|
||||
| **backlog** | `^backlog/` | `prettier --check 'backlog/archive/**/*.md' 'backlog/completed/**/*.md' 'backlog/tasks/**/*.md' 'backlog/docs/**/*.md'` |
|
||||
| **presto** | `^presto/` | `cd presto && mise run test` |
|
||||
| **docker** | `^(Dockerfile\|\.dockerignore\|compose\.yaml)` | `mise run dev:validate-docker-image` (only if `Dockerfile` in staged files) |
|
||||
|
||||
> **† `mix deps.unlock --unused` sub-gate**: Although listed under the elixir category, this check has an additional sub-gate — it runs only when `mix.exs` or `mix.lock` is in the staged files, not on every Elixir change. This is because an unused dependency can only be introduced by changing the dependency specification, not by changing application code.
|
||||
|
||||
If no files match any category (e.g., `git commit --allow-empty`), the script exits early with a message.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Restructure `scripts/dev/precommit`
|
||||
|
||||
Read staged file list from `$STAGED` (already exported by `.git/hooks/pre-commit`). Replace the linear execution with conditionally-guarded blocks using the structure:
|
||||
|
||||
```bash
|
||||
if echo "$STAGED" | grep -qE '<category-pattern>'; then
|
||||
debug_msg "Running <check description>..."
|
||||
<check command>
|
||||
fi
|
||||
```
|
||||
|
||||
Each guard tests whether any staged file matches its category pattern. Each check block runs independently — `set -e` ensures fail-fast behaviour (unchanged from current).
|
||||
|
||||
**Key design decisions**:
|
||||
|
||||
- Prettier blocks are split so each glob group runs only when its category has changes (backlog→backlog prettier, docs→docs prettier, assets→assets/pi prettier). This avoids running prettier on `docs/` when only `backlog/` changed, and vice versa.
|
||||
- The assets prettier command excludes `node_modules` inside `.pi/extensions/` via the `'!.pi/extensions/**/node_modules/**'` negation pattern (prettier supports `.gitignore`-style negation).
|
||||
- The docs prettier command now includes `AGENTS.md` alongside `README.md` and docs globs.
|
||||
- `mix test` runs as `mix test` (same as current) — not partitioned. The pre-commit is for fast feedback; partitioning is a CI concern.
|
||||
- `mix deps.unlock --unused` has a **sub-gate** on `mix.exs` or `mix.lock` changes specifically (not all Elixir changes). See the footnote in the category table.
|
||||
- `mix gettext.extract --check-up-to-date` gates on Elixir changes (it needs compiled code, so any Elixir change could affect gettext extraction).
|
||||
|
||||
**Update the `#MISE description` comment** at the top of the script to: `"Run checks before a commit (conditional on staged file types)"`.
|
||||
|
||||
**File**: `scripts/dev/precommit`
|
||||
|
||||
**Verification**:
|
||||
|
||||
- Stage only a `backlog/tasks/*.md` file: `git add backlog/tasks/some-task.md && mise run dev:precommit`. Confirm only prettier on backlog runs. Confirm `mix test` does NOT run.
|
||||
- Stage only a `presto/main.py` file: confirm only presto pytest runs.
|
||||
- Stage only a `lib/music_library/records.ex` file: confirm all Elixir checks run. Confirm `mix deps.unlock --unused` does **NOT** run (sub-gate: mix.exs/mix.lock not staged).
|
||||
- Stage `mix.exs` only: confirm `mix deps.unlock --unused` **does** run. Confirm other Elixir checks run. Confirm `mix test` runs (any Elixir change triggers tests).
|
||||
- Stage files from all categories simultaneously: confirm all checks run.
|
||||
- `git commit --allow-empty -m "test"`: confirm script exits early without errors.
|
||||
|
||||
### Step 2: Add presto test check
|
||||
|
||||
Add a guard for `^presto/` that runs `cd presto && mise run test`. This uses the presto mise task defined in `presto/mise.toml` (`pytest tests/ -v`).
|
||||
|
||||
**Verification**: Stage `presto/main.py`, run precommit, confirm pytest executes.
|
||||
|
||||
### Step 3: Add Docker image validation
|
||||
|
||||
Add a guard for `Dockerfile` changes that runs `mise run dev:validate-docker-image`. This check already exists in CI lint; adding it to pre-commit catches builder image problems before push.
|
||||
|
||||
**Verification**: Stage `Dockerfile`, run precommit, confirm validate-docker-image executes.
|
||||
|
||||
### Step 4: Manual integration test
|
||||
|
||||
Run through each category in isolation and combined:
|
||||
|
||||
```bash
|
||||
# Elixir-only (without mix.exs/mix.lock — deps.unlock should NOT run)
|
||||
git add lib/music_library/records.ex && mise run dev:precommit
|
||||
|
||||
# Elixir dependency change (deps.unlock SHOULD run)
|
||||
git add mix.exs && mise run dev:precommit
|
||||
|
||||
# Docs-only
|
||||
git add docs/architecture.md && mise run dev:precommit
|
||||
|
||||
# AGENTS.md (docs category, must trigger docs prettier including AGENTS.md)
|
||||
git add AGENTS.md && mise run dev:precommit
|
||||
|
||||
# Backlog-only
|
||||
git add backlog/tasks/some-task.md && mise run dev:precommit
|
||||
|
||||
# Presto-only
|
||||
git add presto/main.py && mise run dev:precommit
|
||||
|
||||
# Shell-only
|
||||
git add scripts/dev/precommit && mise run dev:precommit
|
||||
|
||||
# Docker-only
|
||||
git add Dockerfile && mise run dev:precommit
|
||||
|
||||
# All categories
|
||||
git add lib/ test/ scripts/ docs/ backlog/ presto/ Dockerfile && mise run dev:precommit
|
||||
```
|
||||
|
||||
**Verification**: For each case, observe which checks run (via `debug_msg` output) and confirm only expected checks execute.
|
||||
|
||||
### Step 5: Update documentation
|
||||
|
||||
- **`docs/project-conventions.md`**: Add a "Pre-commit Hooks" subsection under "Workflow" documenting the conditional check behavior and the file-to-check mapping table. Copy the category table from this plan (including the `deps.unlock` sub-gate footnote). Add a note that CI always runs the full suite unconditionally.
|
||||
- **`docs/available-tasks.md`**: Update the `dev:precommit` description from `"Run checks before a commit"` to `"Run checks before a commit (conditional on staged file types)"`.
|
||||
|
||||
## Architecture Impact Analysis
|
||||
|
||||
| Touchpoint | Impact |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `.git/hooks/pre-commit` | No change — already exports `$STAGED` and delegates to `mise run dev:precommit` |
|
||||
| `scripts/dev/precommit` | **Primary change** — restructured from linear to conditional execution |
|
||||
| `scripts/_helpers.sh` | No change needed |
|
||||
| `mise.toml` | No change — `dev:precommit` task definition unchanged (runs `scripts/dev/precommit`) |
|
||||
| `presto/mise.toml` | No change — `test` task already exists |
|
||||
| CI workflows | No change — CI runs full suite unconditionally (correct for CI) |
|
||||
| Supervision tree | No impact |
|
||||
| PubSub topics | No impact |
|
||||
| Schemas / contexts | No impact |
|
||||
| External APIs | No impact |
|
||||
| Routes / UI components | No impact |
|
||||
|
||||
## Performance Profile
|
||||
|
||||
| Scenario | Before | After | Improvement |
|
||||
| ------------------------ | --------------------- | ---------------------- | ------------- |
|
||||
| Backlog/doc change only | ~minutes (full suite) | ~2 seconds (prettier) | ~100x |
|
||||
| Presto change only | ~minutes (full suite) | ~10 seconds (pytest) | ~10x |
|
||||
| Shell script change only | ~minutes (full suite) | ~1 second (shellcheck) | ~100x |
|
||||
| Dockerfile change only | ~minutes (full suite) | ~3 seconds (validate) | ~50x |
|
||||
| Elixir change | ~minutes (full suite) | ~minutes (full Elixir) | No regression |
|
||||
| All categories | ~minutes (full suite) | ~minutes (full suite) | No regression |
|
||||
|
||||
**Runtime complexity**: O(1) — grep on the staged file list is a single pass. Each check block is independently gated.
|
||||
|
||||
**Database query patterns**: Unchanged. Tests still run the same queries when they execute.
|
||||
|
||||
**Memory footprint**: Unchanged — bash script, no persistent state.
|
||||
|
||||
## Production Changes
|
||||
|
||||
None. This is a local development tooling change. The pre-commit hook only runs on developer machines, never in production or CI.
|
||||
|
||||
## Documentation Updates
|
||||
|
||||
1. **`docs/project-conventions.md`**: Add a "Pre-commit Hooks" subsection under "Workflow" with:
|
||||
- Description of conditional check behavior
|
||||
- File-to-check mapping table (same as above, including deps.unlock sub-gate footnote)
|
||||
- Note that CI always runs the full suite
|
||||
|
||||
2. **`docs/available-tasks.md`**: Update `dev:precommit` description from `"Run checks before a commit"` to `"Run checks before a commit (conditional on staged file types)"`
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
|
||||
## Original Draft Notes
|
||||
|
||||
Current state: pre-commit hooks run the entire verification suite irrespective of the change.
|
||||
|
||||
Annoyances identified:
|
||||
|
||||
1. Changes to /backlog items only need the pretty linter.
|
||||
2. Changes to documentation only need the pretty linter.
|
||||
3. Changes to the presto application only need to run the presto test suite.
|
||||
4. Possibly some other patterns, need to verify the complete repo structure.
|
||||
|
||||
## Analysis Summary
|
||||
|
||||
### Current pre-commit flow
|
||||
|
||||
1. `.git/hooks/pre-commit` gathers staged files via `git diff-index --cached --name-only HEAD`, exports `$STAGED` and `$MISE_PRE_COMMIT=1`, then runs `mise run dev:precommit`
|
||||
2. `scripts/dev/precommit` runs all checks linearly: shellcheck → credo → sobelow → gettext → format → prettier → deps.unlock → tests
|
||||
3. No inspection of `$STAGED` — all checks run always
|
||||
|
||||
### File categories identified in repo
|
||||
|
||||
- **Elixir**: `lib/`, `test/`, `config/`, `priv/repo/migrations/`, `priv/gettext/`, `mix.exs`, `.credo.exs`, `.formatter.exs`, `.sobelow-conf`
|
||||
- **Shell**: `scripts/`, `.shellcheckrc`
|
||||
- **Assets/JS/CSS**: `assets/`, `.pi/extensions/`
|
||||
- **Documentation**: `docs/`, `README.md`, `AGENTS.md`
|
||||
- **Backlog**: `backlog/`
|
||||
- **Presto (MicroPython)**: `presto/`
|
||||
- **Docker/Infra**: `Dockerfile`, `.dockerignore`, `compose.yaml`
|
||||
- **Config/misc**: `mise.toml`, `.gitignore` (fall through to Elixir checks since these affect the build)
|
||||
|
||||
### Presto test suite
|
||||
|
||||
- Lives in `presto/tests/` (Python, pytest)
|
||||
- Runnable via `cd presto && mise run test` (uses monorepo config root)
|
||||
- Already exists and is functional
|
||||
|
||||
### Docker validation
|
||||
|
||||
- `mise run dev:validate-docker-image` exists and is used in CI
|
||||
- Not currently in pre-commit — adding it closes a CI→local gap
|
||||
|
||||
### Implementation complete
|
||||
|
||||
**Step 1**: Restructured `scripts/dev/precommit` with conditional guards for 7 categories: shell, elixir, deps (sub-gate), assets, docs, backlog, presto, docker.
|
||||
|
||||
**Step 2**: Added presto check via `(cd presto && mise run test)` gated on `^presto/`.
|
||||
|
||||
**Step 3**: Added Docker image validation via `mise run dev:validate-docker-image` gated on docker pattern.
|
||||
|
||||
**Step 4**: Verified pattern matching for all categories and edge cases (empty STAGED, mix.exs sub-gate, combined categories). Ran real integration test: backlog-only triggered only prettier; Elixir triggered credo → sobelow chain.
|
||||
|
||||
**Step 5**: Updated `docs/project-conventions.md` with Pre-commit Hooks subsection (category table + sub-gate footnote). Updated `docs/available-tasks.md` dev:precommit description.
|
||||
|
||||
### Commit
|
||||
|
||||
Committed as `974ef45` - `ML-187: restructure pre-commit checks to run conditionally`. Pre-commit hook verified: ran only shellcheck + docs prettier for the staged shell/docs files (no Elixir checks, no tests).
|
||||
|
||||
Also fixed pre-existing prettier issue in `AGENTS.md`.
|
||||
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
|
||||
## Summary
|
||||
|
||||
Restructured `scripts/dev/precommit` to run only relevant checks based on staged file types, replacing the linear "run everything" approach.
|
||||
|
||||
### Changes
|
||||
|
||||
- **`scripts/dev/precommit`**: Replaced linear execution with 7 conditionally-gated check categories (shell, elixir, deps, assets, docs, backlog, presto, docker). Each gated behind `grep -qE` on `$STAGED`. Added `deps.unlock` sub-gate (only when `mix.exs` or `mix.lock` staged). Added presto and docker checks that were missing entirely. Added early exit for empty STAGED. Updated MISE description.
|
||||
|
||||
- **`docs/project-conventions.md`**: Added "Pre-commit Hooks" subsection under Workflow with file-to-check mapping table and `deps.unlock` sub-gate footnote.
|
||||
|
||||
- **`docs/available-tasks.md`**: Updated `dev:precommit` description to note conditional behavior.
|
||||
|
||||
### Verification
|
||||
|
||||
Pattern matching verified for all 7 categories in isolation and combined. Real integration test: backlog-only triggered only prettier; Elixir triggered credo→sobelow chain (skipped deps.unlock when no mix.exs/mix.lock staged). Empty STAGED exits early.
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -1,121 +0,0 @@
|
||||
---
|
||||
id: ML-189
|
||||
title: Make Release component's dynamic tag type-safe
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: "2026-05-19 08:42"
|
||||
updated_date: "2026-05-19 11:13"
|
||||
labels:
|
||||
- audit
|
||||
- liveview
|
||||
- components
|
||||
- type-safety
|
||||
dependencies: []
|
||||
documentation:
|
||||
- >-
|
||||
audits/phase1-async-message-coverage/doc-24 -
|
||||
Audit-Report-LiveComponent-→-Parent-handle_info-Coverage-Phase-1.md
|
||||
modified_files:
|
||||
- lib/music_library_web/components/release.ex
|
||||
- lib/music_library_web/live/scrobble_live/release_show.ex
|
||||
- lib/music_library_web/live/collection_live/show.ex
|
||||
priority: low
|
||||
ordinal: 24000
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
The Release component (lib/music_library_web/components/release.ex:82) uses `send(self(), {tag, release})` where `tag` is a dynamic assign (`on_release_loaded`). While correctly handled today, if a future consumer sets a non-atom tag, pattern matching in `handle_info` would fail silently since clauses match on atoms like `:release_loaded`.
|
||||
|
||||
**Fix:**
|
||||
|
||||
1. Remove the `on_release_loaded` attribute from the Release component — the nil-check conditional is an anomaly; every other LiveComponent in the codebase sends messages unconditionally.
|
||||
2. Always send `{__MODULE__, {:loaded, release}}` from `notify_release_loaded/2`.
|
||||
3. Update ScrobbleLive.ReleaseShow's `handle_info` to match `{MusicLibraryWeb.Components.Release, {:loaded, release}}` and remove the `on_release_loaded={:release_loaded}` assign from its template.
|
||||
4. Add a no-op `handle_info` clause in CollectionLive.Show so the message is properly consumed (not silently dropped as an unhandled message).
|
||||
|
||||
**Source:** Audit doc-24 (Phase 1), Recommendation #1.
|
||||
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [ ] #1 Release component's `notify_release_loaded/2` always sends `{__MODULE__, {:loaded, release}}` (no nil check, no `on_release_loaded` attribute)
|
||||
- [ ] #2 ScrobbleLive.ReleaseShow: `handle_info` matches `{MusicLibraryWeb.Components.Release, {:loaded, release}}`, and `on_release_loaded={:release_loaded}` removed from template
|
||||
- [ ] #3 CollectionLive.Show: has a no-op `handle_info({MusicLibraryWeb.Components.Release, {:loaded, _release}}, socket)` clause
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. Release component (`lib/music_library_web/components/release.ex`)
|
||||
|
||||
In `notify_release_loaded/2`, remove the `case`/nil-check and always send:
|
||||
|
||||
```elixir
|
||||
defp notify_release_loaded(_socket, release) do
|
||||
send(self(), {__MODULE__, {:loaded, release}})
|
||||
end
|
||||
```
|
||||
|
||||
Remove the `on_release_loaded` attribute from the LiveComponent — it was only used by `notify_release_loaded/2` and is no longer needed.
|
||||
|
||||
### 2. ScrobbleLive.ReleaseShow (`lib/music_library_web/live/scrobble_live/release_show.ex`)
|
||||
|
||||
Update `handle_info` to match the new message shape:
|
||||
|
||||
```elixir
|
||||
def handle_info({MusicLibraryWeb.Components.Release, {:loaded, release}}, socket) do
|
||||
{:noreply, assign(socket, :page_title, page_title(release))}
|
||||
end
|
||||
```
|
||||
|
||||
Remove `on_release_loaded={:release_loaded}` from the `<.live_component>` call in the template.
|
||||
|
||||
### 3. CollectionLive.Show (`lib/music_library_web/live/collection_live/show.ex`)
|
||||
|
||||
Add a no-op `handle_info` clause (place it alongside the existing component-message handlers around line 338-469):
|
||||
|
||||
```elixir
|
||||
def handle_info({MusicLibraryWeb.Components.Release, {:loaded, _release}}, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
```
|
||||
|
||||
### Verification
|
||||
|
||||
1. Open a release from the scrobble flow (ScrobbleLive.ReleaseShow) — page title should update with the release name after loading.
|
||||
2. Open a release sheet from a record in CollectionLive.Show — the sheet should render correctly with no unhandled-message warnings.
|
||||
3. Run `mix test test/music_library_web/components/release_test.exs` — all existing Release component tests pass.
|
||||
4. Confirm `on_release_loaded` no longer appears in either LiveView template or the Release component.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
|
||||
### Changes
|
||||
|
||||
**1. Release component** (`lib/music_library_web/components/release.ex`):
|
||||
|
||||
- `notify_release_loaded/2` now always sends `{__MODULE__, {:loaded, release}}` — removed the nil-check and `on_release_loaded` attribute-based conditional.
|
||||
|
||||
**2. ScrobbleLive.ReleaseShow** (`lib/music_library_web/live/scrobble_live/release_show.ex`):
|
||||
|
||||
- Updated `handle_info` to match `{MusicLibraryWeb.Components.Release, {:loaded, release}}`.
|
||||
- Removed `on_release_loaded={:release_loaded}` assign from the template.
|
||||
|
||||
**3. CollectionLive.Show** (`lib/music_library_web/live/collection_live/show.ex`):
|
||||
|
||||
- Added no-op `handle_info({MusicLibraryWeb.Components.Release, {:loaded, _release}}, socket)` to consume the message (previously silently dropped).
|
||||
|
||||
All 21 tests pass (12 release component + 3 release_show + 6 collection_show).
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
---
|
||||
id: ML-190
|
||||
title: Skip redundant unsubscribe+resubscribe on same-record reconnect
|
||||
status: Done
|
||||
assignee:
|
||||
- Codex
|
||||
created_date: "2026-05-19 08:42"
|
||||
updated_date: "2026-05-19 11:06"
|
||||
labels:
|
||||
- audit
|
||||
- pubsub
|
||||
- optimization
|
||||
dependencies: []
|
||||
documentation:
|
||||
- >-
|
||||
audits/phase2-pubsub-lifecycle/doc-25 -
|
||||
Audit-Report-PubSub-Subscription-Lifecycle-Phase-2.md
|
||||
modified_files:
|
||||
- lib/music_library_web/live_helpers/record_actions.ex
|
||||
- test/music_library_web/live_helpers/record_actions_test.exs
|
||||
priority: low
|
||||
ordinal: 25000
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
`RecordActions.manage_subscription/2` manages record show PubSub topics from `handle_params/3`. Phoenix.PubSub allows duplicate subscriptions for the same PID/topic and delivers duplicate events, so same-record parameter handling must leave the existing subscription untouched instead of subscribing again.
|
||||
|
||||
**Fix:** Make `manage_subscription/2` distinguish first subscription, different-record navigation, and same-record no-op. First mount subscribes to the record topic; navigating to a different record unsubscribes the old topic and subscribes the new topic; same-record handling does nothing.
|
||||
|
||||
**Source:** Audit doc-25 (Phase 2), Recommendation #2, corrected after verifying Phoenix.PubSub duplicate-subscription behavior.
|
||||
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [x] #1 manage_subscription/2 leaves the existing PubSub subscription untouched when the assigned record ID already matches the new ID
|
||||
- [x] #2 manage_subscription/2 still unsubscribes from the old record and subscribes to the new record when navigating between different records
|
||||
- [x] #3 A regression test verifies same-record parameter handling does not create duplicate PubSub deliveries
|
||||
- [x] #4 Relevant tests pass
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
|
||||
## Overview
|
||||
|
||||
Correct the same-record subscription optimization so it avoids both sides of the redundant PubSub lifecycle. Phoenix.PubSub allows duplicate subscriptions for the same PID/topic and will deliver duplicate events, so same-record handling must not call `Records.subscribe/1` again.
|
||||
|
||||
## Implementation
|
||||
|
||||
1. Update `lib/music_library_web/live_helpers/record_actions.ex` so `manage_subscription/2` has explicit branches for first subscription, different-record navigation, and same-record no-op.
|
||||
2. Add focused regression tests in `test/music_library_web/live_helpers/record_actions_test.exs` using a connected `Phoenix.LiveView.Socket` and the real `Records.subscribe/1` / `Records.notify_update/1` PubSub path.
|
||||
3. Verify the same-record case delivers one broadcast once, and the different-record case stops receiving the old topic while receiving the new topic.
|
||||
4. Run the focused helper/show tests and format the changed files.
|
||||
5. Update this Backlog task to replace the previous incorrect PubSub deduplication note with the verified behavior.
|
||||
|
||||
## Documentation
|
||||
|
||||
No project architecture, production infrastructure, or convention docs need codebase-level changes because this is a bug fix within the existing `LiveHelpers.RecordActions.manage_subscription/2` pattern. The necessary documentation updates are the local `@doc` for `manage_subscription/2` and this task record, since it previously claimed Phoenix.PubSub deduplicates duplicate subscriptions.
|
||||
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
|
||||
Found that Phoenix.PubSub duplicate subscriptions are allowed and produce duplicate deliveries; the previous task notes/final summary claiming PID/topic deduplication were incorrect.
|
||||
|
||||
Updated `manage_subscription/2` to treat same-record handling as a no-op, preserving first-subscribe and different-record switch behavior. Added regression tests that prove one broadcast is delivered once after same-record handling and that different-record navigation drops the old topic.
|
||||
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
|
||||
Updated `RecordActions.manage_subscription/2` so it no-ops when the assigned record ID already matches the requested record ID. This prevents duplicate Phoenix.PubSub subscriptions, which would otherwise deliver duplicate `{:update, record}` messages for one broadcast.
|
||||
|
||||
The helper now has explicit branches for first subscription, different-record navigation, and same-record no-op. Its `@doc` was corrected to describe the actual behavior.
|
||||
|
||||
Added focused regression coverage in `test/music_library_web/live_helpers/record_actions_test.exs`:
|
||||
|
||||
- same-record subscription handling does not duplicate PubSub deliveries
|
||||
- navigating between records unsubscribes the old topic and subscribes the new topic
|
||||
|
||||
Documentation update: corrected this Backlog task's description, plan, notes, and final summary. No project architecture, production infrastructure, or convention docs required changes because the behavior remains within the existing LiveView subscription pattern.
|
||||
|
||||
Tests run:
|
||||
`mix test test/music_library_web/live_helpers/record_actions_test.exs test/music_library_web/live/collection_live/show_test.exs test/music_library_web/live/wishlist_live/show_test.exs` -> 23 passed
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
Reference in New Issue
Block a user