mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-25 22:31:48 +00:00
* feat(v3 library): practice-aware home — Repertoire meter + "Keep practicing" shelf The Songs page opened cold into a flat sorted grid. This adds a practice-aware front door on the unfiltered grid, built entirely from data already on hand (no new endpoints, no new stored state): - Repertoire meter — "Repertoire: N of M songs · K in progress" + a bar, counting songs at/above the same mastery threshold the green accuracy badge uses (>= 0.9 best accuracy) over the unfiltered library total. Reads state.accuracy (/api/stats/best, already loaded for the card badges) and the unfiltered /api/library/stats total. - "Keep practicing" shelf — a horizontal row of recently-played, not-yet- mastered songs (newest first, click to play). Reads /api/stats/recent. Both show ONLY on the grid view when not searching/filtering/selecting (the front-door context), refresh after a song is scored (applyScoreRefresh), and collapse on an empty library. Soft-gamification only: descriptive encouragement (goal-gradient / endowed-progress), never content-gating, decay, or nagging — the practice-accuracy "continue" rail a media server can't do. Frontend-only: static/v3/songs.js (renderLibraryHome / _repertoireCounts / libHomeVisible, wired through reload() + applyScoreRefresh), static/v3/v3.css. Came out of the library design charrette (UX + gamification lenses' top pick). Stacked on the A–Z rail branch (feat/v3-library-az-rail) since both touch static/v3/songs.js; merge that PR first (or retarget). Tests: tests/js/v3_keep_practicing.test.js (threshold, front-door gating, shelf filter, denominator, render/reload/score-refresh wiring, click-to-play). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(v3 library): correct practice-aware home for review P1/P2/P3 Addresses the PR #635 review findings (manual + Codex): P1 correctness - Gate the Repertoire meter + "Keep practicing" shelf to the LOCAL provider (libHomeVisible). They read local practice stats (state.accuracy / /api/stats/recent); on a remote provider they mixed a local mastered count with a remote song total (e.g. "85 of 80") and the shelf played local files while browsing a remote library. - Shelf now gates on the per-SONG best (state.accuracy[filename] = MAX across arrangements, what the green badge shows) and dedupes by filename, instead of the per-arrangement recents row — so a "keep practicing" card can no longer show a green "mastered" badge, and a song can't appear twice. P2 robustness - renderLibraryHome fetches /api/library/stats + /api/stats/recent together (Promise.all) and a _homeToken generation guard discards a stale render so a slow response can't repaint a home the grid already moved past. P3 polish - accuracyBadge references MASTERY_ACCURACY instead of a bare 0.9, so the badge and the meter/shelf can't drift from "the same mastery threshold". Tests updated (v3_keep_practicing.test.js): provider gating, per-song deduped shelf, Promise.all + token. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
75 lines
3.5 KiB
JavaScript
75 lines
3.5 KiB
JavaScript
// Pins the practice-aware library home in static/v3/songs.js:
|
|
// - a "Repertoire" progress meter (mastered / total library songs), and
|
|
// - a "Keep practicing" shelf (recently played, not yet mastered).
|
|
// Both reuse existing data (/api/stats/best already in state.accuracy, and
|
|
// /api/stats/recent) and are shown only on the unfiltered grid front door.
|
|
//
|
|
// Source-level only — same strategy as tests/js/v3_az_rail.test.js.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const SONGS_JS = path.join(__dirname, '..', '..', 'static', 'v3', 'songs.js');
|
|
const src = fs.readFileSync(SONGS_JS, 'utf8');
|
|
|
|
test('repertoire uses the same mastery threshold as the green accuracy badge', () => {
|
|
assert.match(src, /const\s+MASTERY_ACCURACY\s*=\s*0\.9/);
|
|
assert.match(
|
|
src,
|
|
/function\s+_repertoireCounts[\s\S]*?v\s*>=\s*MASTERY_ACCURACY\s*\)\s*mastered\+\+;\s*else\s+learning\+\+/,
|
|
'repertoire counts must bucket scored songs into mastered/learning at MASTERY_ACCURACY',
|
|
);
|
|
});
|
|
|
|
test('the home is the unfiltered grid front door, local provider only', () => {
|
|
assert.match(
|
|
src,
|
|
/function\s+libHomeVisible[\s\S]*?state\.view === 'grid'[\s\S]*?state\.provider === 'local'[\s\S]*?!state\.selectMode[\s\S]*?!state\.q[\s\S]*?activeFilterCount\(\)\s*===\s*0/,
|
|
'libHomeVisible must require grid view, the local provider, no select mode, no search, no active filters',
|
|
);
|
|
});
|
|
|
|
test('the shelf is recently-played, not-yet-mastered songs (per-song, deduped)', () => {
|
|
assert.match(src, /\/api\/stats\/recent\?limit=/);
|
|
// Mastery is gated on the per-SONG best (state.accuracy, what the badge
|
|
// shows), not the per-arrangement recents row, and each filename appears
|
|
// once — so no green-badged "keep practicing" card and no duplicates.
|
|
assert.match(
|
|
src,
|
|
/const\s+best\s*=\s*acc\[r\.filename\][\s\S]*?best\s*>=\s*MASTERY_ACCURACY/,
|
|
'the shelf must gate on the per-song best (state.accuracy) at MASTERY_ACCURACY',
|
|
);
|
|
assert.match(src, /seen\.has\(r\.filename\)/, 'the shelf must dedupe recents by filename');
|
|
});
|
|
|
|
test('the meter + shelf fetch together and a stale render is discarded', () => {
|
|
assert.match(src, /Promise\.all\(\[[\s\S]*?library\/stats[\s\S]*?stats\/recent/,
|
|
'the two reads must be issued together (Promise.all), not sequentially');
|
|
assert.match(src, /_homeToken[\s\S]*?_homeToken !== myToken/,
|
|
'a stale render must be superseded by a newer one via a token');
|
|
});
|
|
|
|
test('the repertoire denominator is the unfiltered library total', () => {
|
|
assert.match(src, /\/api\/library\/stats\?provider='/);
|
|
assert.match(src, /total_songs\s*\?\?\s*stats\.total/);
|
|
assert.match(src, /Math\.round\(\(mastered\s*\/\s*total\)\s*\*\s*100\)/);
|
|
});
|
|
|
|
test('the home + #v3-lib-home host are wired into render and reload', () => {
|
|
assert.match(src, /id="v3-lib-home"/, 'render() must include the #v3-lib-home host');
|
|
assert.match(src, /function reload\s*\([\s\S]*?updateLibraryHome\(\)/,
|
|
'reload() must refresh/toggle the home');
|
|
assert.match(src, /function applyScoreRefresh[\s\S]*?renderLibraryHome\(\)/,
|
|
'a new score must refresh the meter + shelf');
|
|
});
|
|
|
|
test('shelf cards play the song on click', () => {
|
|
assert.match(
|
|
src,
|
|
/querySelectorAll\('\.v3-kp-card'\)[\s\S]*?window\.playSong\(enc\(fn\)/,
|
|
'a shelf card click must call window.playSong with the recents filename',
|
|
);
|
|
});
|