feat(v3 library): practice-aware home — Repertoire meter + "Keep practicing" shelf (#635)

* 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>
This commit is contained in:
ChrisBeWithYou
2026-06-29 08:56:31 +02:00
committed by GitHub
co-authored by Claude Opus 4.8 byrongamatos
parent 6a71577e05
commit 6a6efc793a
4 changed files with 236 additions and 3 deletions
+30
View File
@@ -1189,3 +1189,33 @@ html.fb-immersive #v3-main > .screen.active {
@media (max-height: 640px) {
.v3-azrail-letter { font-size: .55rem; padding: 0 4px; }
}
/* — Practice-aware library home: repertoire meter + "Keep practicing" shelf — */
#v3-lib-home.hidden { display: none; }
.v3-rep-meter { max-width: 30rem; }
.v3-rep-track {
height: 6px;
border-radius: 999px;
background: rgba(148, 163, 184, .22); /* fb-textDim @ low alpha */
overflow: hidden;
}
.v3-rep-fill {
height: 100%;
border-radius: 999px;
background: #0ea5e9; /* fb-primary */
transition: width .4s ease;
}
/* Horizontal, scroll-snapping shelf of fixed-width cards. */
.v3-kp-row {
display: flex;
gap: .75rem;
overflow-x: auto;
scroll-snap-type: x proximity;
padding-bottom: 6px;
-webkit-overflow-scrolling: touch;
}
.v3-kp-card {
flex: 0 0 8.5rem;
width: 8.5rem;
scroll-snap-align: start;
}