From b103a722ceb7b7fcfdc62c9c38bf064c71714a99 Mon Sep 17 00:00:00 2001 From: ChrisBeWithYou Date: Sun, 28 Jun 2026 06:52:48 -0500 Subject: [PATCH] fix(v3): refresh Songs grid after a Settings rescan / DLC-folder change (#624) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported on macOS: on a fresh install, pointing at a DLC folder in Settings and running a scan showed NO songs until an app restart. The scan itself was fine — _background_scan re-reads config.json fresh, so it scans the new folder and populates the library — but the v3 Songs grid never reloaded. The Settings Rescan / Full Rescan handlers only refreshed the classic (v2) library via loadLibrary(); the v3 grid (static/v3/songs.js) had no listener for a scan it didn't initiate (only its own upload path self-refreshes via watchUploadScan). So its cached, pre-DLC (empty) DOM/snapshot survived a sidebar return until a full reload (restart). Fix: the rescan handlers now emit `library:changed` (static/app.js). The v3 grid listens and reloads if it's the active screen, else sets `_libraryDirty` so the next onV3SongsScreenEnter does a full re-fetch — a short-circuit placed ahead of every cached-DOM fast-path so it can't restore the stale grid. Tests: tests/js/v3_library_refresh.test.js guards the emit + the reload/dirty wiring (DOM/event glue isn't headlessly unit-testable; end-to-end wants an in-app run of the reporter's flow: set DLC in Settings → scan → Songs populate without restart). Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 Co-authored-by: byrongamatos --- CHANGELOG.md | 1 + static/app.js | 6 +++++ static/v3/songs.js | 21 +++++++++++++++ tests/js/v3_library_refresh.test.js | 40 +++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 tests/js/v3_library_refresh.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index bc9bfe0..09e0798 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **v3 library: exact artist/album filters + scroll/page-depth restore** (feedBack#857). The v3 Songs toolbar gains Artist and Album dropdowns (Album populates from the selected artist and stays disabled until one is chosen), backed by new exact, case-insensitive (`COLLATE NOCASE`) `artist` / `album` query params threaded through `MetadataDB._build_where` → `query_page` / `query_artists` / `query_stats` and the `/api/library`, `/api/library/artists`, `/api/library/stats` endpoints (the free-text `q` search stays fuzzy and composes with the exact filters). The artist/album catalog is fetched independently of the active artist/album selection so the dropdowns always list the full set for the current provider/search. The toolbar is now sticky so filter controls stay reachable when browsing deep libraries, and returning from the player restores the previous scroll position **and** the loaded infinite-scroll page depth via a `sessionStorage` snapshot keyed by a filter/sort/view state hash (invalidated whenever those change, so a filter change still resets to the top). Tests: `tests/test_library_filters.py` (backend artist/album filters), `tests/js/v3_songs_scroll.test.js` (state-hash + snapshot helpers). ### Fixed +- **v3 Songs grid now refreshes after a Settings rescan / DLC-folder change — no app restart needed.** On a fresh install, pointing at a DLC folder in Settings and running a scan left the Songs section empty until a restart (the scan *did* populate the library — `_background_scan` re-reads `config.json` fresh — but the v3 grid never reloaded). The Settings **Rescan / Full Rescan** handlers only refreshed the classic (v2) library via `loadLibrary()`; the v3 grid (`static/v3/songs.js`) had no listener for a scan it didn't start itself (only its own upload path self-refreshed via `watchUploadScan`), so its cached, pre-DLC (empty) DOM/snapshot survived a sidebar return until a full reload. The rescan handlers now emit a **`library:changed`** event (`static/app.js`); the v3 grid listens and **reloads if it's the active screen, else marks itself dirty** so the next entry does a full re-fetch instead of restoring the stale snapshot (a `_libraryDirty` short-circuit ahead of every cached-DOM fast-path in `onV3SongsScreenEnter`). Tests: `tests/js/v3_library_refresh.test.js` (the emit + the reload/dirty wiring). - **Edit Metadata modal: the Year is now editable.** You could set a year when authoring a pak but the Songs → Edit Metadata modal had no Year field, so it could never be changed afterward. The backend (`POST /api/song//meta`) already accepted and normalized `year` (writes it into the file via `songmeta`, survives a rescan) — only the UI omitted it. Added a **Year** input to `openEditModal()` (populated from the song's existing year) and included `year` in `saveEditModal()`'s POST body (`static/app.js`). Both the v3 card menu and the legacy edit button already pass the year through, so both surfaces get the field. - **Edit Metadata modal no longer closes when a click-drag is released on the backdrop.** Selecting text inside a field and releasing the mouse past the modal's edge dismissed the form without warning (the `click` event's target resolved to the backdrop), discarding the edit. Backdrop dismissal now requires the **mousedown to have started on the backdrop** too — tracked per-modal and decided by a new pure `_editModalShouldClose(clickTarget, modalEl, downOnBackdrop)` helper (`static/app.js`). Cancel / ✕ still close on a normal click. Tests: `tests/js/edit_metadata_modal.test.js` (year in the POST body + the backdrop-close decision table). - **Built-in diagnostic sloppak rebranded "Slopsmith" → "FeedBack" in the song name.** PR #586 renamed the file to `feedBack-diagnostic-basic-guitar.sloppak` but never regenerated the archive, so the manifest inside still carried `title: Slopsmith Diagnostic — Basic Guitar` / `artist: Slopsmith` (and the same heading in `DIAGNOSTIC.md`) — the stale name testers saw in the library/player and the onboarding calibration step, even though the build script, server, and docs all already say "FeedBack Diagnostic — Basic Guitar". Regenerated `docs/diagnostics/feedBack-diagnostic-basic-guitar.sloppak` from `docs/diagnostics/build_diagnostic_basic_guitar.py` so the committed artifact matches its source generator (title/artist/heading now "FeedBack"; chart, stem, and `diagnostic:` metadata unchanged). No code change — the rename in #586 just needed the rebuild. diff --git a/static/app.js b/static/app.js index 701c328..64ca346 100644 --- a/static/app.js +++ b/static/app.js @@ -4543,6 +4543,9 @@ async function rescanLibrary() { _treeStats = null; _tuningNames = null; // re-fetch on next drawer open loadLibrary(); + // Tell the v3 Songs grid the library changed so it reloads instead of + // keeping a cached (e.g. pre-DLC, empty) grid until an app restart. + if (window.feedBack) window.feedBack.emit('library:changed', { reason: 'rescan' }); } }, 1000); } @@ -4571,6 +4574,9 @@ async function fullRescanLibrary() { _treeStats = null; _tuningNames = null; // re-fetch on next drawer open loadLibrary(); + // Tell the v3 Songs grid the library changed so it reloads instead of + // keeping a cached (e.g. pre-DLC, empty) grid until an app restart. + if (window.feedBack) window.feedBack.emit('library:changed', { reason: 'rescan' }); } }, 1000); } diff --git a/static/v3/songs.js b/static/v3/songs.js index 82551dd..577bece 100644 --- a/static/v3/songs.js +++ b/static/v3/songs.js @@ -338,6 +338,12 @@ // tracks filenames scored while the library was off-screen, applied on enter. const _dirtyScores = new Set(); + // Set when a library scan / DLC-folder change happened while this screen was + // off (or showing a stale, e.g. pre-DLC empty, grid). The grid's cached DOM / + // snapshot would otherwise survive a sidebar return, so we force a full + // re-fetch on the next entry. (feedBack — "No DLC until restart".) + let _libraryDirty = false; + function repaintAccuracy(key) { const apply = (el, variant) => { if (el.getAttribute('data-fn') !== key) return; @@ -1049,6 +1055,10 @@ } async function onV3SongsScreenEnter() { + // A library scan / DLC-folder change marked the grid stale — re-fetch + // from scratch instead of restoring a cached (possibly empty, pre-DLC) + // snapshot. Must win over every fast-path below. + if (_libraryDirty) { _libraryDirty = false; await reload(); return; } // Pull in any scores recorded while the library was off-screen (the usual // play→return flow) before the fast-paths below restore the cached DOM, // so the just-played song's badge is current. The full render() path @@ -1202,5 +1212,16 @@ const active = document.querySelector('.screen.active'); if (active && active.id === 'v3-songs') applyScoreRefresh(); }); + // A library scan (rescan / full rescan from Settings, or a DLC-folder + // change) can add or remove songs while this grid is cached — the + // Settings rescan only refreshed the classic library, so the v3 grid + // stayed on its pre-scan (e.g. empty, pre-DLC) state until an app + // restart. Reload now if we're showing; otherwise mark dirty so the next + // entry re-fetches instead of restoring the stale snapshot. + sm.on('library:changed', () => { + const active = document.querySelector('.screen.active'); + if (active && active.id === 'v3-songs') { _libraryDirty = false; reload(); } + else _libraryDirty = true; + }); } })(); diff --git a/tests/js/v3_library_refresh.test.js b/tests/js/v3_library_refresh.test.js new file mode 100644 index 0000000..31cefc0 --- /dev/null +++ b/tests/js/v3_library_refresh.test.js @@ -0,0 +1,40 @@ +// Regression guard for "No DLC until restart": a library scan triggered from +// Settings (rescan / full rescan, e.g. right after pointing at a DLC folder) +// reloaded only the classic library — the v3 Songs grid kept its cached +// (pre-DLC, empty) state until an app restart. +// +// The fix wires a `library:changed` event (emitted by the rescan handlers in +// app.js) to a reload in static/v3/songs.js. That's DOM/event glue, not a pure +// function, so these are source-level guards that the wiring isn't dropped; the +// end-to-end behavior is verified in-app / by a browser test. + +'use strict'; +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const root = path.join(__dirname, '..', '..'); +const SONGS = fs.readFileSync(path.join(root, 'static', 'v3', 'songs.js'), 'utf8'); +const APP = fs.readFileSync(path.join(root, 'static', 'app.js'), 'utf8'); + +test('app.js emits library:changed when a Settings rescan completes', () => { + assert.match(APP, /emit\(\s*['"]library:changed['"]/, + 'a completed rescan must broadcast library:changed for the v3 grid'); +}); + +test('songs.js handles library:changed — reload when active, else mark dirty', () => { + const m = SONGS.match(/sm\.on\(\s*['"]library:changed['"][\s\S]{0,500}?\}\);/); + assert.ok(m, 'songs.js must subscribe to library:changed'); + assert.match(m[0], /reload\(\)/, 'reloads the grid when the screen is active'); + assert.match(m[0], /_libraryDirty\s*=\s*true/, 'marks dirty when off-screen'); +}); + +test('onV3SongsScreenEnter forces a reload when the library is dirty', () => { + const m = SONGS.match(/function onV3SongsScreenEnter\(\)[\s\S]{0,400}?\{/); + assert.ok(m, 'onV3SongsScreenEnter present'); + // The dirty check must short-circuit to a reload before the cached-DOM + // fast-paths get a chance to restore the stale grid. + assert.match(SONGS, /if\s*\(_libraryDirty\)\s*\{[^}]*reload\(\)[^}]*return;/, + 'a dirty library must force a full reload on entry, ahead of any fast-path'); +});