From af2949677ad0a1b498226fb49e6f1677d8748c55 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Tue, 23 Jun 2026 02:03:01 -0700 Subject: [PATCH] =?UTF-8?q?rename:=20slopsmith=20=E2=86=92=20feedBack,=20b?= =?UTF-8?q?yron=20=E2=86=92=20got-feedBack=20(#537)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update GitHub repo references from feedback* to feedBack* * rename: slopsmith -> feedBack, byron -> got-feedBack Renames across the entire codebase: - slopsmith/Slopsmith/SLOPSMITH/SlopSmith -> feedBack/FeedBack/FEEDBACK/FeedBack - byron/Byron/Byrongamatos -> got-feedBack/got-feedBack/got-feedBack - /home/byron/ -> /opt/got-feedBack/ - byron@ougsoft.com -> hi@got-feedBack.org - github.com/byrongamatos/ -> github.com/got-feedback/ - com.byron. -> com.got-feedback. - SLOPSMITH_ env vars -> FEEDBACK_ with backward-compat fallback - Protocol/storage strings migrated with read-old/write-new pattern - window.slopsmith JS API -> window.feedBack (canonical) + backward-compat alias Refs: #rename-slopsmith * rename: complete regen against current main + fix backward-compat alias Regenerated the slopsmith->feedBack / byron->got-feedBack rename on top of current main (3 commits had landed since the branch: #572/#554/#574), resolving the four content conflicts in favour of main's newer content (autoplay/auto-exit, accuracy-badge, Virtuoso re-home, feedpak badge). Completion fixes on top of the mechanical rename: - Re-apply rename to post-branch content the original rename never saw: window.slopsmith(.Tour) consumers in lessons.js / notifications.js / onboarding-tour.js, and the matching JS + python tests (autoplay_exit, progression_*, test_feedpak_extension FEEDBACK_* env vars). The test env vars now match server.py (which reads FEEDBACK_SYNC_STARTUP / FEEDBACK_SKIP_STARTUP_TASKS), so the sync-startup test exercises the real path again. - Restore the window.slopsmith backward-compat alias dropped during conflict resolution, and move the bus aliases to AFTER the _feedBackExisting merge block so they reference the fully-assembled object (also fixes the loop_api.test.js API-surface regex, which the original PR latently broke). - Drop the stray empty data/web_library.db (runtime DB lives in CONFIG_DIR) and gitignore it. - Fix stale tone-source test: feed[dB]ack -> fee[dB]ack to match shipped source labels. Verified locally (org CI billing-blocked): JS 819/819 pass; pytest 1669 passed / 1683 collected with 0 import errors; zero residual slopsmith/byron except the two intentional window.slopsmith aliases. Co-Authored-By: Claude Opus 4.8 (1M context) * rename: implement advertised backward-compat + prune dead community plugins Address gaps where PR #537's "Backward compatibility" section was advertised but not implemented, and clean up the community plugin list. Env vars (FEEDBACK_* canonical, legacy SLOPSMITH_* honoured): - New lib/env_compat.py (getenv_compat / env_flag_compat) + tests. server.py (_env_flag + all FEEDBACK_* reads), diagnostics_hardware, gp2midi and tailwind_rebuild now resolve the legacy alias, so existing SLOPSMITH_UI / SLOPSMITH_PLUGINS_DIR / etc. deployments keep working. - Fix the rename collapsing plugins/__init__.py and minigames/routes.py from `FEEDBACK_PLUGINS_DIR or SLOPSMITH_PLUGINS_DIR` into a redundant `FEEDBACK_ or FEEDBACK_` (the fallback was silently lost). Storage (app.js update-channel): - Read feedBack-update-channel, fall back to legacy slopsmith-update-channel, and clear the legacy key on write — so a user's update-channel preference survives the rename instead of resetting to "stable". Community plugin list (README): the rename rewrote third-party repo URLs we don't own. Probed every one; their owners never renamed, so: - Restore the 13 live community plugins to their real slopsmith-* names. - Prune 6 that are 404 to the public (topkoa splitscreen/stems, OmikronApex tuner, Jafz2001 nam-rig-builder, DeathlySin song-preview, Erikcb91 shuffle). - Fix a pre-existing Guitar Theory clone-command typo (nam-tone -> guitar-theory). Verified: env_compat 7/7, JS 819/819, pytest 1690 collected / 0 import errors, rename-sensitive + startup suites green. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: byrongamatos Co-authored-by: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 4 +- .github/workflows/sync-version.yml | 4 +- .gitignore | 2 + .specify/memory/constitution.md | 22 +- CHANGELOG.md | 66 +- CLAUDE.md | 108 ++-- CONTRIBUTING.md | 12 +- Dockerfile | 30 +- README.md | 50 +- SUPPORTERS.md | 4 +- TODO.md | 2 +- build-proxmox-ct.sh | 24 +- docker-compose.nas.yml | 8 +- docker-compose.yml | 6 +- docs/NOTE_FAILURE_PLAN.md | 6 +- docs/NOTE_FAILURE_SPEC.md | 12 +- .../note_detect_bass_v1/BENCHMARK.md | 2 +- .../note_detect_bass_v1/build_benchmark.py | 14 +- docs/benchmarks/note_detect_v1/BENCHMARK.md | 6 +- .../note_detect_v1/build_benchmark.py | 18 +- docs/benchmarks/note_detect_v2/BENCHMARK.md | 2 +- .../note_detect_v2/build_benchmark.py | 12 +- docs/capability-domains.md | 48 +- docs/capability-recipes.md | 36 +- docs/capability-roadmap.md | 10 +- docs/capability-safety-matrix.md | 8 +- docs/diagnostics-bundle-spec.md | 40 +- docs/diagnostics/README.md | 20 +- .../build_diagnostic_basic_guitar.py | 14 +- docs/note-detect-tuning.md | 22 +- docs/plugin-capability-inventory.md | 10 +- docs/plugin-manifest.schema.json | 4 +- docs/plugin-styles.md | 6 +- docs/plugin-v3-ui.md | 16 +- docs/skills/debugging-keyboard-shortcuts.md | 6 +- docs/sloppak-hand-editing.md | 34 +- docs/sloppak-spec.md | 8 +- lib/audio.py | 6 +- lib/diagnostics_bundle.py | 48 +- lib/diagnostics_hardware.py | 6 +- lib/diagnostics_redact.py | 2 +- lib/drums.py | 2 +- lib/env_compat.py | 38 ++ lib/gp2midi.py | 22 +- lib/gp2notation.py | 6 +- lib/gp2rs.py | 4 +- lib/gp2rs_gpx.py | 4 +- lib/gp8_audio_sync.py | 4 +- lib/gp_autosync.py | 2 +- lib/logging_setup.py | 22 +- lib/lyrics_transcribe.py | 8 +- lib/notation.py | 2 +- lib/scan_worker.py | 8 +- lib/sloppak.py | 4 +- lib/song.py | 16 +- lib/songmeta.py | 2 +- lib/tailwind_rebuild.py | 10 +- lib/tones.py | 6 +- lib/vocal_pitch.py | 12 +- lib/wem_decode.py | 2 +- main.py | 2 +- package-lock.json | 4 +- package.json | 4 +- plugins/__init__.py | 40 +- plugins/app_tour_library/script.js | 18 +- plugins/app_tour_library/tour.json | 2 +- plugins/app_tour_settings/script.js | 16 +- plugins/app_tour_settings/tour.json | 4 +- plugins/capability_inspector/screen.js | 16 +- plugins/highway_3d/CLAUDE.md | 30 +- plugins/highway_3d/README.md | 12 +- plugins/highway_3d/routes.py | 4 +- plugins/highway_3d/screen.js | 134 ++--- plugins/highway_3d/settings.html | 4 +- plugins/highway_3d/tailwind.config.js | 2 +- plugins/input_setup/screen.js | 14 +- plugins/minigames/README.md | 24 +- plugins/minigames/routes.py | 8 +- plugins/minigames/screen.html | 2 +- plugins/minigames/screen.js | 46 +- plugins/minigames/settings.html | 4 +- plugins/tuner/README.md | 28 +- plugins/tuner/routes.py | 2 +- plugins/tuner/screen.js | 50 +- plugins/tuner/settings.html | 4 +- plugins/tuner/utils/audio.js | 2 +- plugins/tuner/utils/ui.js | 42 +- plugins/tuner/visualization/analogue-gauge.js | 2 +- plugins/tuner/visualization/chef-mt3.js | 2 +- plugins/tuner/visualization/default.js | 2 +- plugins/tuner/visualization/mace-fx-iii.js | 2 +- plugins/tuner/visualization/strobe.js | 2 +- plugins/tuner/visualization/toilet-tuner.js | 2 +- scripts/lift_keys_notation.py | 2 +- server.py | 101 ++-- specs/012-midi-input-domain/spec.md | 8 +- specs/013-midi-control-mappings/spec.md | 2 +- static/app.js | 563 +++++++++--------- static/audio-mixer.js | 38 +- static/capabilities.js | 36 +- static/capabilities/audio-effects.js | 24 +- static/capabilities/audio-session.js | 24 +- static/capabilities/library-card-actions.js | 12 +- static/capabilities/library.js | 14 +- static/capabilities/midi-input.js | 14 +- static/capabilities/note-detection.js | 14 +- static/capabilities/playback.js | 18 +- static/capabilities/tuning.js | 8 +- static/capabilities/visualization.js | 28 +- static/diagnostics.js | 14 +- static/highway.js | 120 ++-- static/index.html | 38 +- static/lottie-api.js | 2 +- static/lottie/spinner-original.json | 2 +- static/lottie/spinner.json | 2 +- static/style.css | 4 +- static/tour-engine.css | 44 +- static/tour-engine.js | 56 +- static/v3/audio-routing.js | 2 +- static/v3/badges.js | 2 +- static/v3/card-actions-core.js | 4 +- static/v3/dashboard.js | 4 +- static/v3/feedbarcade.js | 10 +- static/v3/index.html | 42 +- static/v3/lessons.js | 10 +- static/v3/live-guitar-tone-source.js | 2 +- static/v3/live-performance-hud.js | 2 +- static/v3/notifications.js | 6 +- static/v3/onboarding-tour.js | 12 +- static/v3/pedal-cables.js | 2 +- static/v3/player-chrome.js | 12 +- static/v3/playlists.js | 2 +- static/v3/plugins-page.js | 4 +- static/v3/profile.js | 16 +- static/v3/progress.js | 2 +- static/v3/progression-core.js | 6 +- static/v3/shell.js | 6 +- static/v3/shop.js | 2 +- static/v3/songs.js | 6 +- static/v3/stats-recorder.js | 6 +- static/v3/theme-core.js | 8 +- static/v3/venue-mood-fx.js | 6 +- static/v3/venue-scene-3d.js | 4 +- tailwind.config.js | 8 +- tests/browser/README.md | 6 +- tests/browser/audio-session-compat.spec.ts | 8 +- tests/browser/audio-session.spec.ts | 6 +- tests/browser/basic-load.spec.ts | 2 +- tests/browser/check-errors.spec.ts | 4 +- tests/browser/default-arrangement-pin.spec.ts | 12 +- tests/browser/highway-3d-lefty.spec.ts | 2 +- tests/browser/progress-shop.spec.ts | 6 +- tests/browser/wake-lock.spec.ts | 42 +- tests/conftest.py | 6 +- tests/js/audio_effects_host.test.js | 100 ++-- tests/js/audio_effects_test_harness.js | 2 +- tests/js/audio_session_compat.test.js | 42 +- tests/js/audio_session_host.test.js | 26 +- tests/js/audio_session_input.test.js | 110 ++-- tests/js/audio_session_mix.test.js | 32 +- tests/js/audio_session_monitoring.test.js | 102 ++-- tests/js/audio_session_routes.test.js | 34 +- tests/js/audio_session_stems.test.js | 12 +- tests/js/audio_session_stems_bridge.test.js | 8 +- tests/js/audio_session_test_harness.js | 6 +- tests/js/autoplay_exit.test.js | 10 +- tests/js/beats_loaded.test.js | 22 +- tests/js/capabilities_claims.test.js | 4 +- tests/js/capabilities_diagnostics.test.js | 18 +- tests/js/capabilities_lifecycle.test.js | 22 +- .../js/capabilities_manifest_runtime.test.js | 14 +- tests/js/capabilities_multi_provider.test.js | 2 +- tests/js/capabilities_ownership.test.js | 4 +- tests/js/capabilities_test_harness.js | 2 +- tests/js/capabilities_versioning.test.js | 2 +- tests/js/capability_inspector_render.test.js | 78 +-- tests/js/diagnostics_summary.test.js | 10 +- tests/js/highway_3d_arp_deferral.test.js | 2 +- tests/js/highway_3d_lefty.test.js | 2 +- tests/js/highway_3d_panel_controls.test.js | 8 +- tests/js/highway_3d_pool_warm.test.js | 2 +- .../js/highway_3d_smooth_clock_pause.test.js | 2 +- tests/js/highway_adaptive_scale.test.js | 2 +- tests/js/highway_audio_barrier.test.js | 12 +- tests/js/highway_chord_render_cache.test.js | 2 +- tests/js/highway_colors_facade.test.js | 8 +- tests/js/highway_note_state.test.js | 6 +- tests/js/highway_pause_throttle.test.js | 2 +- tests/js/highway_string_colors.test.js | 4 +- tests/js/highway_visibility.test.js | 16 +- tests/js/juce_engine_reroute.test.js | 8 +- tests/js/legacy_shim_hits.test.js | 24 +- tests/js/live_guitar_tone_source.test.js | 2 +- tests/js/loop_api.test.js | 18 +- tests/js/loop_restart.test.js | 8 +- tests/js/midi_input_domain.test.js | 62 +- tests/js/note_detection_domain.test.js | 62 +- tests/js/pedal_cables.test.js | 2 +- tests/js/playback_app_adapter.test.js | 4 +- tests/js/playback_compat.test.js | 22 +- tests/js/playback_diagnostics.test.js | 20 +- tests/js/playback_domain.test.js | 46 +- tests/js/playback_test_harness.js | 6 +- tests/js/plugins_page.test.js | 2 +- tests/js/progress_calibration.test.js | 20 +- tests/js/progression_notifications.test.js | 2 +- tests/js/progression_progress_events.test.js | 2 +- tests/js/song_close.test.js | 4 +- tests/js/song_event_payload.test.js | 6 +- tests/js/song_restart.test.js | 6 +- tests/js/song_seek.test.js | 2 +- tests/js/speed_reset.test.js | 4 +- tests/js/tour_engine.test.js | 8 +- tests/js/tuner_auto_open.test.js | 18 +- tests/js/tuning_display.test.js | 6 +- tests/js/tuning_targets.test.js | 4 +- tests/js/v3_songs_tuning.test.js | 4 +- tests/js/venue_mood_fx.test.js | 16 +- tests/js/venue_scene_3d.test.js | 22 +- tests/js/venue_viz.test.js | 10 +- tests/js/visualization_domain.test.js | 72 +-- tests/test_audio_bundled.py | 16 +- tests/test_audio_effect_mappings.py | 2 +- tests/test_audio_local_path.py | 8 +- tests/test_builtin_diagnostic_seed.py | 8 +- tests/test_correlation_id.py | 16 +- tests/test_demo_mode.py | 34 +- tests/test_diagnostics_bundle.py | 26 +- tests/test_diagnostics_hardware.py | 4 +- tests/test_diagnostics_redact.py | 2 +- tests/test_env_compat.py | 50 ++ tests/test_feedpak_extension.py | 4 +- tests/test_gp2midi.py | 32 +- tests/test_gp2rs.py | 2 +- tests/test_highway_ws_notation.py | 2 +- tests/test_library_filters.py | 12 +- tests/test_library_providers.py | 4 +- tests/test_logging_setup.py | 50 +- tests/test_loose_traversal.py | 2 +- tests/test_minigames_routes.py | 6 +- tests/test_parse_arrangement.py | 2 +- tests/test_playlists_api.py | 2 +- tests/test_plugin_manifest_contract.py | 2 +- tests/test_plugin_runtime_idempotence.py | 30 +- tests/test_plugins.py | 122 ++-- tests/test_profile_api.py | 2 +- tests/test_progression_api.py | 4 +- tests/test_settings_api.py | 16 +- tests/test_settings_export.py | 8 +- tests/test_settings_instrument.py | 2 +- tests/test_sloppak_cover_art.py | 2 +- tests/test_sloppak_file_traversal.py | 4 +- tests/test_song.py | 4 +- tests/test_song_stats_api.py | 2 +- tests/test_startup_status.py | 36 +- tests/test_tailwind_rebuild.py | 8 +- tests/test_version_endpoint.py | 4 +- 257 files changed, 2389 insertions(+), 2293 deletions(-) create mode 100644 lib/env_compat.py create mode 100644 tests/test_env_compat.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9445ad7..0a7ceae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: first=$(printf '%s\n' "$hits" | head -n1) file=$(printf '%s' "$first" | cut -d: -f1) line=$(printf '%s' "$first" | cut -d: -f2) - echo "::error file=${file},line=${line}::print() or traceback.print_exc() found in server.py, lib/, or a bundled plugin routes.py. Use the slopsmith logger (lib/logging_setup.py) — see issues #155 / #242." + echo "::error file=${file},line=${line}::print() or traceback.print_exc() found in server.py, lib/, or a bundled plugin routes.py. Use the feedBack logger (lib/logging_setup.py) — see issues #155 / #242." exit 1 fi @@ -56,7 +56,7 @@ jobs: tailwind-fresh: # Guard that the committed static/tailwind.min.css is in sync with source. - # The Play CDN's runtime JIT was removed (slopsmith-desktop#110); a prebuilt + # The Play CDN's runtime JIT was removed (feedBack-desktop#110); a prebuilt # stylesheet only contains classes the scanner saw at build time, so stale # CSS silently ships unstyled elements. Rebuild and fail on any diff. name: tailwind-fresh diff --git a/.github/workflows/sync-version.yml b/.github/workflows/sync-version.yml index 9262925..6aa07a5 100644 --- a/.github/workflows/sync-version.yml +++ b/.github/workflows/sync-version.yml @@ -1,7 +1,7 @@ name: Sync VERSION from desktop release -# Updates the VERSION file in this repo whenever slopsmith-desktop -# publishes a new tagged release. slopsmith-desktop's build.yml +# Updates the VERSION file in this repo whenever feedBack-desktop +# publishes a new tagged release. feedBack-desktop's build.yml # dispatches the `desktop-released` event at the end of a successful # tag build (see docs in CLAUDE.md). A `workflow_dispatch` trigger is # kept for manual testing / recovery. diff --git a/.gitignore b/.gitignore index 46c3979..c36ed39 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ build/ .env* .DS_Store .vscode/ +data/web_library.db static/*.ogg static/*.mp3 static/*.wav @@ -48,3 +49,4 @@ Thumbs.db *.tmp *.swp .idea/ +plugins/support_creators diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index 713b506..db3b68f 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -1,6 +1,6 @@ -# Slopsmith Constitution +# FeedBack Constitution -> Slopsmith is a self-hosted, single-user web app for browsing, playing, and +> FeedBack is a self-hosted, single-user web app for browsing, playing, and > practicing interactive music notation, built around its own open `.sloppak` > chart format (charts imported from Guitar Pro / MusicXML or authored in the > built-in editor). This constitution captures the non-negotiable principles @@ -13,7 +13,7 @@ ### I. Self-Hosted, Single-User, Docker-First -Slopsmith targets one user running one container against a personal +FeedBack targets one user running one container against a personal song library folder. There is no multi-tenant model, no authentication, no rate limiting, and no shared backend. Deployment is expressed as a single `docker compose up -d` against the bundled @@ -41,12 +41,12 @@ is Tailwind CSS, served as a prebuilt static stylesheet (`static/tailwind.min.css`, regenerated by `scripts/build-tailwind.sh`) — never the runtime Play CDN, whose on-the-fly JIT rescans the DOM on the main thread and caused sustained frame drops with the 3D highway -(slopsmith-desktop#110). No React, Vue, Svelte, bundler, transpiler, or +(feedBack-desktop#110). No React, Vue, Svelte, bundler, transpiler, or TypeScript appears in the core static tree, and no build step runs on the serve path: the Tailwind build is a maintainer-only one-shot whose output is committed, so Docker / desktop / end users never build. New features extend `app.js` and the existing globals (`window.playSong`, -`window.showScreen`, `window.createHighway`, `window.slopsmith`). +`window.showScreen`, `window.createHighway`, `window.feedBack`). **Non-negotiable rules** @@ -89,7 +89,7 @@ do not collide in `sys.modules`. sibling imports. Bare `import sibling` works during transition but triggers a startup warning when a name collides. - Plugins MUST register routes under `/api/plugins//...`, - use `window.slopsmith.emit/on` for cross-plugin communication, and + use `window.feedBack.emit/on` for cross-plugin communication, and prefix their `localStorage` keys with their plugin id. - Plugins inherit this constitution and may layer additional rules in their own `CLAUDE.md`, but MUST NOT relax core principles (e.g. a @@ -97,7 +97,7 @@ do not collide in `sys.modules`. ### IV. Backwards-Compatible Chart Library -The whole point of Slopsmith is that a user points it at an existing +The whole point of FeedBack is that a user points it at an existing song library folder and it Just Works. The library is scanned and indexed in `meta.db` (SQLite via `MetadataDB`). The open Sloppak format (`lib/sloppak.py`; specified at @@ -145,7 +145,7 @@ push and PR to `main` against Python 3.12. All backend output goes through the stdlib `logging` pipeline configured by `lib/logging_setup.py`, controlled by `LOG_LEVEL` / `LOG_FORMAT` / `LOG_FILE`. Plugins receive a pre-configured `context["log"]` namespaced -to `slopsmith.plugin.` and MUST use it instead of `print`. HTTP +to `feedBack.plugin.` and MUST use it instead of `print`. HTTP responses carry a `X-Request-ID` header from `CorrelationIdMiddleware` and the same id appears as `request_id` in JSON log lines. The "Settings → Export Diagnostics" bundle (`lib/diagnostics_bundle.py`) @@ -171,7 +171,7 @@ User configuration lives in two places: server-side under `CONFIG_DIR` (SQLite `meta.db`, `config.yaml`, plugin opted-in files) and client- side in browser `localStorage`. Both can be exported and re-imported as a single bundle (`POST /api/settings/import`, -`GET /api/settings/export`, slopsmith#113). Import is two-phase: +`GET /api/settings/export`, feedBack#113). Import is two-phase: phase-1 validates the entire bundle (schema, paths, encoding) and phase-2 commits each file atomically via temp+rename. Plugins opt their server-side files into the bundle via @@ -188,7 +188,7 @@ no `..`, no absolute paths). Importing a bundle whose schema predates the running plugin's code MUST restore bytes verbatim — the plugin copes at next load. - The `VERSION` file is the single source of truth for the running - release; it is auto-bumped from `slopsmith-desktop` releases via + release; it is auto-bumped from `feedBack-desktop` releases via `.github/workflows/sync-version.yml`. Manual edits are reserved for out-of-band recovery only. @@ -219,7 +219,7 @@ no `..`, no absolute paths). - **Branching**: never push directly to `main`. Always feature branch + PR. Exception: the automated `VERSION` bump from - `slopsmith-desktop`'s release job, which commits to `main` as + `feedBack-desktop`'s release job, which commits to `main` as `github-actions[bot]`. - **Reviews**: PRs run the local Codex review loop (`feedback_codex_preflight.md`) and the GitHub Copilot review pass diff --git a/CHANGELOG.md b/CHANGELOG.md index 560a099..e6bf47f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- **Autoplay & auto-exit — a global "click it, it plays; finish, you're back at the menu" option (default ON).** New single Settings toggle (`autoplayExit` in `localStorage`, surfaced in both the v3 and classic settings screens; absence of the key = enabled) that closes the friction at both ends of the play loop. **Autoplay:** `playSong()` previously loaded a chart paused, requiring a Play press; a one-shot flag armed per fresh load is now consumed by the next `song:ready` (highway.js) to auto-start via the existing `togglePlay()` path (HTML5 + `_juceMode` + count-in). Arrangement switches / seeks reuse the same `song:ready` event but never arm the flag, so they don't auto-restart. **Auto-exit:** on `song:ended`, core returns to the launching menu after a short grace delay — unless a visible full-screen results/dialog overlay is on top (detected via `[role=dialog][aria-modal]` / `.fixed.inset-0` with a `getClientRects()` visibility test that works for `position:fixed`), in which case the return is deferred so that score screen's own Close button (calling `window.closeCurrentSong()`) drives the exit. A plugin can also defer explicitly via the new `window.slopsmith.holdAutoExit()` (called synchronously from its own `song:ended` handler — core's listener runs first). Both paths mean **no external plugin PR is required** for a results screen to be respected. **Context-aware destination:** the player's remembered origin (`_playerOriginScreen`) now honours any real launch screen instead of clamping to library/home/favorites, and a one-shot `window.slopsmith.setReturnScreen(id)` override lets the lessons catalog (`static/v3/lessons.js`) send a finished lesson back to the lessons screen — not the song library — even though the external tutorials plugin owns the `playSong` call. Also exposes a read-only `window.slopsmith.autoplayExit` getter for plugins. Songs and lessons share the same `playSong` → highway path, so both inherit the behaviour. Core-only (`static/app.js`, `static/v3/lessons.js`, both `index.html`s); the end-of-song score screen itself remains a plugin. Optional polish (not required — the overlay heuristic already covers it): external scoring/note-detection plugins (e.g. SlopScale) may call `holdAutoExit()` + `closeCurrentSong()` for an exact, heuristic-free handoff. +- **Autoplay & auto-exit — a global "click it, it plays; finish, you're back at the menu" option (default ON).** New single Settings toggle (`autoplayExit` in `localStorage`, surfaced in both the v3 and classic settings screens; absence of the key = enabled) that closes the friction at both ends of the play loop. **Autoplay:** `playSong()` previously loaded a chart paused, requiring a Play press; a one-shot flag armed per fresh load is now consumed by the next `song:ready` (highway.js) to auto-start via the existing `togglePlay()` path (HTML5 + `_juceMode` + count-in). Arrangement switches / seeks reuse the same `song:ready` event but never arm the flag, so they don't auto-restart. **Auto-exit:** on `song:ended`, core returns to the launching menu after a short grace delay — unless a visible full-screen results/dialog overlay is on top (detected via `[role=dialog][aria-modal]` / `.fixed.inset-0` with a `getClientRects()` visibility test that works for `position:fixed`), in which case the return is deferred so that score screen's own Close button (calling `window.closeCurrentSong()`) drives the exit. A plugin can also defer explicitly via the new `window.feedBack.holdAutoExit()` (called synchronously from its own `song:ended` handler — core's listener runs first). Both paths mean **no external plugin PR is required** for a results screen to be respected. **Context-aware destination:** the player's remembered origin (`_playerOriginScreen`) now honours any real launch screen instead of clamping to library/home/favorites, and a one-shot `window.feedBack.setReturnScreen(id)` override lets the lessons catalog (`static/v3/lessons.js`) send a finished lesson back to the lessons screen — not the song library — even though the external tutorials plugin owns the `playSong` call. Also exposes a read-only `window.feedBack.autoplayExit` getter for plugins. Songs and lessons share the same `playSong` → highway path, so both inherit the behaviour. Core-only (`static/app.js`, `static/v3/lessons.js`, both `index.html`s); the end-of-song score screen itself remains a plugin. Optional polish (not required — the overlay heuristic already covers it): external scoring/note-detection plugins (e.g. SlopScale) may call `holdAutoExit()` + `closeCurrentSong()` for an exact, heuristic-free handoff. - **"Song Editor" promoted to a first-class v3 sidebar item.** The editor plugin (`id: editor`) now gets its own dedicated sidebar entry — under the Library group, just below Songs — via the existing `PROMOTED_PLUGINS` @@ -17,48 +17,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 (`renderPromotedNav` checks `/api/plugins`), so it appears only when the editor is loaded. The displayed label comes from the plugin's manifest `nav.label`. -- **Guitar Pro → notation importer (`lib/gp2notation.py`)** (slopsmith#825 WS4b, epic #828). Piano/keys tracks imported from Guitar Pro (GPIF: `.gpx` GP6 / `.gp` GP7-8) now produce real Sloppak Notation Format data (sloppak-spec §5.3) alongside the `midi = string*24 + fret` guitar wire encoding. `gp2rs_gpx.convert_file` writes a `.notation.json` sidecar next to each keys arrangement XML (best-effort — a notation bug never breaks the RS-XML conversion), and `gp2notation.attach_notation_to_sloppak()` is the assembly-side helper that renames it into `notation_.json` + adds the per-arrangement `notation:` manifest sub-key. Voice→staff routing salvages the logic from PR #703 (whose `stf` wire-field approach this supersedes): GP voice position 0 → `rh` staff (`G2`), positions ≥ 1 → `lh` (`F4`); a forced-LH track (the merged `Piano LH` partner from `_find_piano_pairs`, or a standalone track named `… LH`) routes everything to `lh` — preserving authored hand crossings instead of inferring hands from pitch. Emits measures with absolute `t` from the bar-indexed tempo map, change-only `ts`/`tempo`/`ks`, and `beat_groups` for compound/irregular meters (6/8 → `[3,3]`, 9/8 → `[3,3,3]`, 5/8 → `[2,3]`, 7/8 → `[2,2,3]` — cf. the slopsmith#261 denominator pitfalls); beats carry `dur`/`dot`/`tu`/`rest` from GP rhythms and notes carry absolute `midi` (String+Fret resolves via the string template's concert pitches, Tone+Octave via `(octave+1)*12 + step`) with `tied` continuations kept as real beats (engraving needs the tied notehead — unlike the RS-XML walk, which drops them and extends sustain). Timing reuses the `gp2rs_gpx` machinery (bar-indexed tempo map, per-beat rhythm durations, `_note_midi`) so notation lines up with the RS XML the highway plays — with one deliberate divergence: double dots advance time ×1.75 (vs. the RS-XML walk's single-dot ×1.5 approximation) so a written `dot: 2` agrees with the emitted beat times; sharing the walk itself stays tracked in slopsmith#618. Tests: `tests/test_gp2notation.py`. -- **Legacy keys → notation lifter (`scripts/lift_keys_notation.py`)** (slopsmith#825 WS4c, epic #828). One-time batch converter that lifts existing **directory-form** piano/keys sloppaks from the legacy guitar wire encoding (`midi = s*24 + f`) into real Sloppak Notation Format files (sloppak-spec §5.3). Candidates are arrangements whose name matches `\b(keys|piano|keyboard|synth)\b` (case-insensitive); each gets a `notation_.json` plus the per-arrangement `notation:` manifest sub-key. Measures derive from the song-level `beats` downbeats (`measure >= 0`; `song_timeline.json` preferred, first-arrangement fallback), with per-measure tempo from downbeat spacing (emitted only on a > 1 BPM change). Durations come from the wire sustain (`sus`, legacy `l` alias) when present, else the gap to the next onset in the same hand — quantized to the nearest plain/single-dotted `{1,2,4,8,16,32}` denominator at the local tempo, floored at a 32nd. Hands are split heuristically: onsets within 10 ms form a group; a group spanning > 12 semitones splits at its largest internal interval gap (low side → `lh`), otherwise the whole group goes by mean pitch vs middle C — single-staff output when everything lands on one hand. Idempotent (arrangements already carrying `notation:` are skipped; an orphan `notation_.json` without the manifest key is refused, not overwritten) with `--dry-run` support; every payload is checked via `notation.validate_notation` before write. Honest caveat: the manifest is round-tripped through PyYAML (`safe_load` + `safe_dump(sort_keys=False)`) — key order survives, YAML comments/custom formatting do not (the script warns when comments are present). Zip-form `.sloppak` files are reported and skipped. Tests: `tests/test_lift_keys_notation.py`. -- **Notation schema v1 freeze — completeness batch** (slopsmith#822, epic #828). Adds the low-hanging-fruit fields ahead of content production: top-level credits `rights`/`lyricist`/`arranger`; measure `pickup` (anacrusis); beat `arp` (arpeggiate), `ferm` (fermata), and **typed grace notes** — `grace: "a"` (acciaccatura, MusicXML `grace/@slash=yes`) / `"p"` (appoggiatura); note `stem` (`"up"`/`"down"` force). Pedal is settled as the existing `spd`/`sph`/`spu` trio with a documented MusicXML `` mapping — no separate `ped` field. A new "v1 non-features" spec subsection pins the accepted limitations (microtonal, figured bass, mid-measure key/time/clef changes, `ott`/`barline`/ornaments/`trem`/`glis`) as additive-v1.x territory. `lib/notation.py` gains the `GRACE_TYPES`, `STEM_DIRECTIONS`, and `DYNAMICS` vocabularies; the validator stays permissive by design. -- **Notation format — standard musical notation as a first-class sloppak type.** Promotes keys, piano, violin, and any other staff-notation instrument out of the guitar wire format and into their own data structure, following the same promotion path used for drums (slopsmith#344). New `lib/notation.py` defines the canonical vocabulary (`CLEFS`, `DURATIONS`, `SCHEMA_VERSION`), a permissive `validate_notation()` check, and `measures_to_wire()` / `measure_to_wire()` wire helpers. `lib/sloppak.py::load_song` reads a new per-arrangement `notation:` sub-key from each arrangement entry in the manifest (Option B: per-arrangement, not song-wide), applies path-traversal guards, validates the parsed JSON via `validate_notation()`, and surfaces all notation payloads on `LoadedSloppak.notation_by_id` (a `dict[str, dict]` keyed by arrangement id). A failed or missing notation file for one arrangement does not abort or skip the arrangement itself — partial-failure isolation mirrors the drum tab loader. `file:` is now optional when `notation:` is present: the loader creates a minimal stub arrangement so a notation-only arrangement entry does not require a guitar wire format JSON. `/ws/highway/{filename}` gains two new message types — `notation_info` (staves, instrument, total measure count) and chunked `notation_measures` (32 measures per chunk) — streamed after `sections` and before `anchors`; `song_info` carries a new `has_notation: bool` flag so viz pickers can auto-activate the notation plugin regardless of arrangement name. The notation file schema is measure-structured (`measure → staff → voice → beat → note`), uses MIDI for pitch (no string/fret/tuning indirection), and carries the full set of effects that alphaTab can render. See `docs/sloppak-spec.md` §5.3 for the full schema. Open questions resolved per the piano/keys epic (slopsmith#828 / #822): Option B (per-arrangement `notation:` sub-key) and `file:`-optional-when-`notation:`-present are the endorsed design. +- **Guitar Pro → notation importer (`lib/gp2notation.py`)** (feedBack#825 WS4b, epic #828). Piano/keys tracks imported from Guitar Pro (GPIF: `.gpx` GP6 / `.gp` GP7-8) now produce real Sloppak Notation Format data (sloppak-spec §5.3) alongside the `midi = string*24 + fret` guitar wire encoding. `gp2rs_gpx.convert_file` writes a `.notation.json` sidecar next to each keys arrangement XML (best-effort — a notation bug never breaks the RS-XML conversion), and `gp2notation.attach_notation_to_sloppak()` is the assembly-side helper that renames it into `notation_.json` + adds the per-arrangement `notation:` manifest sub-key. Voice→staff routing salvages the logic from PR #703 (whose `stf` wire-field approach this supersedes): GP voice position 0 → `rh` staff (`G2`), positions ≥ 1 → `lh` (`F4`); a forced-LH track (the merged `Piano LH` partner from `_find_piano_pairs`, or a standalone track named `… LH`) routes everything to `lh` — preserving authored hand crossings instead of inferring hands from pitch. Emits measures with absolute `t` from the bar-indexed tempo map, change-only `ts`/`tempo`/`ks`, and `beat_groups` for compound/irregular meters (6/8 → `[3,3]`, 9/8 → `[3,3,3]`, 5/8 → `[2,3]`, 7/8 → `[2,2,3]` — cf. the feedBack#261 denominator pitfalls); beats carry `dur`/`dot`/`tu`/`rest` from GP rhythms and notes carry absolute `midi` (String+Fret resolves via the string template's concert pitches, Tone+Octave via `(octave+1)*12 + step`) with `tied` continuations kept as real beats (engraving needs the tied notehead — unlike the RS-XML walk, which drops them and extends sustain). Timing reuses the `gp2rs_gpx` machinery (bar-indexed tempo map, per-beat rhythm durations, `_note_midi`) so notation lines up with the RS XML the highway plays — with one deliberate divergence: double dots advance time ×1.75 (vs. the RS-XML walk's single-dot ×1.5 approximation) so a written `dot: 2` agrees with the emitted beat times; sharing the walk itself stays tracked in feedBack#618. Tests: `tests/test_gp2notation.py`. +- **Legacy keys → notation lifter (`scripts/lift_keys_notation.py`)** (feedBack#825 WS4c, epic #828). One-time batch converter that lifts existing **directory-form** piano/keys sloppaks from the legacy guitar wire encoding (`midi = s*24 + f`) into real Sloppak Notation Format files (sloppak-spec §5.3). Candidates are arrangements whose name matches `\b(keys|piano|keyboard|synth)\b` (case-insensitive); each gets a `notation_.json` plus the per-arrangement `notation:` manifest sub-key. Measures derive from the song-level `beats` downbeats (`measure >= 0`; `song_timeline.json` preferred, first-arrangement fallback), with per-measure tempo from downbeat spacing (emitted only on a > 1 BPM change). Durations come from the wire sustain (`sus`, legacy `l` alias) when present, else the gap to the next onset in the same hand — quantized to the nearest plain/single-dotted `{1,2,4,8,16,32}` denominator at the local tempo, floored at a 32nd. Hands are split heuristically: onsets within 10 ms form a group; a group spanning > 12 semitones splits at its largest internal interval gap (low side → `lh`), otherwise the whole group goes by mean pitch vs middle C — single-staff output when everything lands on one hand. Idempotent (arrangements already carrying `notation:` are skipped; an orphan `notation_.json` without the manifest key is refused, not overwritten) with `--dry-run` support; every payload is checked via `notation.validate_notation` before write. Honest caveat: the manifest is round-tripped through PyYAML (`safe_load` + `safe_dump(sort_keys=False)`) — key order survives, YAML comments/custom formatting do not (the script warns when comments are present). Zip-form `.sloppak` files are reported and skipped. Tests: `tests/test_lift_keys_notation.py`. +- **Notation schema v1 freeze — completeness batch** (feedBack#822, epic #828). Adds the low-hanging-fruit fields ahead of content production: top-level credits `rights`/`lyricist`/`arranger`; measure `pickup` (anacrusis); beat `arp` (arpeggiate), `ferm` (fermata), and **typed grace notes** — `grace: "a"` (acciaccatura, MusicXML `grace/@slash=yes`) / `"p"` (appoggiatura); note `stem` (`"up"`/`"down"` force). Pedal is settled as the existing `spd`/`sph`/`spu` trio with a documented MusicXML `` mapping — no separate `ped` field. A new "v1 non-features" spec subsection pins the accepted limitations (microtonal, figured bass, mid-measure key/time/clef changes, `ott`/`barline`/ornaments/`trem`/`glis`) as additive-v1.x territory. `lib/notation.py` gains the `GRACE_TYPES`, `STEM_DIRECTIONS`, and `DYNAMICS` vocabularies; the validator stays permissive by design. +- **Notation format — standard musical notation as a first-class sloppak type.** Promotes keys, piano, violin, and any other staff-notation instrument out of the guitar wire format and into their own data structure, following the same promotion path used for drums (feedBack#344). New `lib/notation.py` defines the canonical vocabulary (`CLEFS`, `DURATIONS`, `SCHEMA_VERSION`), a permissive `validate_notation()` check, and `measures_to_wire()` / `measure_to_wire()` wire helpers. `lib/sloppak.py::load_song` reads a new per-arrangement `notation:` sub-key from each arrangement entry in the manifest (Option B: per-arrangement, not song-wide), applies path-traversal guards, validates the parsed JSON via `validate_notation()`, and surfaces all notation payloads on `LoadedSloppak.notation_by_id` (a `dict[str, dict]` keyed by arrangement id). A failed or missing notation file for one arrangement does not abort or skip the arrangement itself — partial-failure isolation mirrors the drum tab loader. `file:` is now optional when `notation:` is present: the loader creates a minimal stub arrangement so a notation-only arrangement entry does not require a guitar wire format JSON. `/ws/highway/{filename}` gains two new message types — `notation_info` (staves, instrument, total measure count) and chunked `notation_measures` (32 measures per chunk) — streamed after `sections` and before `anchors`; `song_info` carries a new `has_notation: bool` flag so viz pickers can auto-activate the notation plugin regardless of arrangement name. The notation file schema is measure-structured (`measure → staff → voice → beat → note`), uses MIDI for pitch (no string/fret/tuning indirection), and carries the full set of effects that alphaTab can render. See `docs/sloppak-spec.md` §5.3 for the full schema. Open questions resolved per the piano/keys epic (feedBack#828 / #822): Option B (per-arrangement `notation:` sub-key) and `file:`-optional-when-`notation:`-present are the endorsed design. - **`song_timeline.json` — beats and sections as a top-level file.** A new optional top-level file pointed at by a new manifest key (`song_timeline: song_timeline.json`) provides the correct home for song-wide beats and sections, replacing the legacy convention of embedding them in the first arrangement JSON. The loader in `lib/sloppak.py` reads and validates the file (must be a dict with `beats` and `sections` as lists), clears and repopulates `Song.beats` / `Song.sections` from it when present, and stores the raw dict on `LoadedSloppak.song_timeline`. The existing arrangement-JSON fallback is fully preserved: all existing sloppaks that omit `song_timeline:` continue to load without any change. This is a prerequisite for notation-only sloppaks, which may have no arrangement JSON at all and therefore no carrier for beats/sections data. New sloppaks should put beats/sections in `song_timeline.json` only. See `docs/sloppak-spec.md` §2 and §5.3. -- **`note-detection` capability domain promoted — control plane (spec 009)** (slopsmith#727/#728, epic #828). New core host `static/capabilities/note-detection.js`: provider registry (kinds `midi`/`engine`/`js`, primitives `pitch.estimate`/`verify.target`), requester-owned context-scoped detection bindings (`open-binding`/`close-binding`/`set-target`/`clear-target` — each binding carries its own redacted tuning context, independent of the host's loaded song, per spec-009 FR-003), and hit/miss/verdict observability events (consumers own judgment). The legacy chart-coupled `highway.setNoteStateProvider` surface keeps working and is wrapped for compatibility-shim hit accounting. Diagnostics (`slopsmith.note_detection_capability.v1`) carry provider/binding summaries and bounded outcomes — no raw audio, device labels, or song identity. Migrating the chart path, Step Mode verify, minigames YIN, and the engine verifier onto bindings is the remainder of the spec-009 slice. -- **`visualization` capability domain promoted (cap:6)** (slopsmith#828). New core host `static/capabilities/visualization.js` registers a provider-coordinator owning the highway renderer surface: commands `inspect` / `list-providers` / `select-renderer` / `clear-renderer` (selection delegates to the existing picker so persistence, WebGL2 gating, and fallback stay single-sourced), events `providers-refreshed` / `renderer-changed` / `renderer-ready` / `renderer-failed`. Legacy discovery (`type: "visualization"` manifests, `window.slopsmithViz_*` globals) keeps working unchanged and is accounted as compatibility shims with hit counts. `static/app.js` attributes every renderer change (auto-match / user-select / fallback) and auto-match outcomes into the domain. Diagnostics (`slopsmith.visualization_capability.v1`) carry provider ids/labels/context types, active renderer + selection source, last auto-match outcome, and last failure — no song filenames/titles. Per-panel (splitscreen) selection is a tracked follow-up. -- **Viz picker routes notation arrangements** (slopsmith#826, epic #828). `window.slopsmith.currentSong` gains `hasNotation` (sibling of `hasDrumTab`) from the `song_info` frame's `has_notation` flag, so notation viz plugins (Staff View, Keys Highway 3D) can gate `matchesArrangement` on data presence instead of arrangement-name heuristics. When a notation-only arrangement (no wire notes — `file:` omitted per sloppak-spec §5.3) falls through Auto with no notation plugin installed, the built-in highway still takes the canvas but the Auto label reads "no notation view installed" and a one-shot dismissable hint points at the visualization picker — never a silently blank board. -- **Keys instrument path in progression** (slopsmith#828). New `data/progression/paths/keys.json` (5 levels / 15 challenges at parity with the guitar path) plus keys-flavoured daily/weekly quest pool entries (`d.keys-one` "Ivory Tower", `w.keys-three` "Grand Recital"). `lib/progression.py::instrument_for_arrangement()` now attributes `type: piano|keys` arrangements — and names matching `keys`/`piano`/`keyboard`/`synth` on a word boundary — to the new `keys` instrument, so scored keys runs advance the path automatically. Purely content + attribution: no schema or API changes. -- **v3 library: exact artist/album filters + scroll/page-depth restore** (slopsmith#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). +- **`note-detection` capability domain promoted — control plane (spec 009)** (feedBack#727/#728, epic #828). New core host `static/capabilities/note-detection.js`: provider registry (kinds `midi`/`engine`/`js`, primitives `pitch.estimate`/`verify.target`), requester-owned context-scoped detection bindings (`open-binding`/`close-binding`/`set-target`/`clear-target` — each binding carries its own redacted tuning context, independent of the host's loaded song, per spec-009 FR-003), and hit/miss/verdict observability events (consumers own judgment). The legacy chart-coupled `highway.setNoteStateProvider` surface keeps working and is wrapped for compatibility-shim hit accounting. Diagnostics (`feedBack.note_detection_capability.v1`) carry provider/binding summaries and bounded outcomes — no raw audio, device labels, or song identity. Migrating the chart path, Step Mode verify, minigames YIN, and the engine verifier onto bindings is the remainder of the spec-009 slice. +- **`visualization` capability domain promoted (cap:6)** (feedBack#828). New core host `static/capabilities/visualization.js` registers a provider-coordinator owning the highway renderer surface: commands `inspect` / `list-providers` / `select-renderer` / `clear-renderer` (selection delegates to the existing picker so persistence, WebGL2 gating, and fallback stay single-sourced), events `providers-refreshed` / `renderer-changed` / `renderer-ready` / `renderer-failed`. Legacy discovery (`type: "visualization"` manifests, `window.feedBackViz_*` globals) keeps working unchanged and is accounted as compatibility shims with hit counts. `static/app.js` attributes every renderer change (auto-match / user-select / fallback) and auto-match outcomes into the domain. Diagnostics (`feedBack.visualization_capability.v1`) carry provider ids/labels/context types, active renderer + selection source, last auto-match outcome, and last failure — no song filenames/titles. Per-panel (splitscreen) selection is a tracked follow-up. +- **Viz picker routes notation arrangements** (feedBack#826, epic #828). `window.feedBack.currentSong` gains `hasNotation` (sibling of `hasDrumTab`) from the `song_info` frame's `has_notation` flag, so notation viz plugins (Staff View, Keys Highway 3D) can gate `matchesArrangement` on data presence instead of arrangement-name heuristics. When a notation-only arrangement (no wire notes — `file:` omitted per sloppak-spec §5.3) falls through Auto with no notation plugin installed, the built-in highway still takes the canvas but the Auto label reads "no notation view installed" and a one-shot dismissable hint points at the visualization picker — never a silently blank board. +- **Keys instrument path in progression** (feedBack#828). New `data/progression/paths/keys.json` (5 levels / 15 challenges at parity with the guitar path) plus keys-flavoured daily/weekly quest pool entries (`d.keys-one` "Ivory Tower", `w.keys-three` "Grand Recital"). `lib/progression.py::instrument_for_arrangement()` now attributes `type: piano|keys` arrangements — and names matching `keys`/`piano`/`keyboard`/`synth` on a word boundary — to the new `keys` instrument, so scored keys runs advance the path automatically. Purely content + attribution: no schema or API changes. +- **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 - **A song's accuracy badge now updates on its library card right after you play it — no restart needed.** The v3 library (`static/v3/songs.js`) loaded the best-accuracy map (`/api/stats/best`) once into `state.accuracy` at render time and only ever refreshed it on a full re-render; the play→return flow takes the screen-entry fast-path that restores the cached grid DOM without re-fetching, so a just-earned score stayed invisible until the next restart re-ran `render()`. The `stats-recorder` now emits a `stats:recorded` event (carrying `filename`/`arrangement`) once the scored `POST /api/stats` resolves on the server — the correct moment, since `song:stop` fires before the POST completes. `songs.js` listens: if the library is the active screen it re-fetches `/api/stats/best` and patches the affected card/row badge in place; otherwise it marks the filename dirty and `onV3SongsScreenEnter` applies it on return (a failed fetch keeps the entry dirty to retry). Badge markup was factored into a shared `accuracyBadge(filename, variant)` (grid pill + tree-row percentage, both tagged `.fb-acc-badge`) so the in-place `repaintAccuracy` can find and replace them without a full list re-render (scroll/pagination preserved). The old empty `song:stop` "refresh lazily next render" placeholder is replaced. - **Changing Settings → 3D Highway → Fret spacing no longer ejects you to the home screen.** The `highway_3d` plugin's `h3dSetFretSpacing` was the lone 3D-highway setting that called `location.reload()` to apply — and since the SPA boots with `#home` as the active screen (`index.html` `.screen.active`), the reload dropped the user out of Settings onto the homescreen. It now applies live like every other 3D-highway setting: it rebinds the module-scope `_h3dFretUniform` flag (so panels mounted later this session pick up the new mode), recomputes the two `fretX`-derived scalars that were baked at init (`_fretLabelScaleRefW` for fret-label sprite scaling, `FRET_WIDTH_MID` for camera hysteresis), and broadcasts a `fretSpacing` change over the existing `_bgEmitChange` pub-sub so every mounted panel rebuilds its board via `buildBoard()`. Per-frame note geometry already reads `fretX` live and needs no rebuild. No page reload, so the Settings screen stays put. Source-level regression tests in `tests/js/highway_3d_fret_spacing.test.js` now pin the no-reload / live-rebuild behavior. -- **v3 library scroll-restore no longer breaks the classic v2 UI or drops off-screen searches** (slopsmith#857). Two regressions in the scroll-restore work above: (1) `playSong` remapped `home`-launched songs to return to the `#v3-songs` screen unconditionally, but `static/app.js` is shared with the v2 UI (served at `/v2` / `SLOPSMITH_UI=v2`) where that screen does not exist — Esc-from-player then called `showScreen('v3-songs')`, which threw on the missing element and stranded the user on a blank screen with playback still running; the remap now applies only when `#v3-songs` is present. (2) The Songs screen-entry fast-path skips reloading to preserve scroll, but the global topbar search routed through it, so once Songs had been visited, searching from another screen navigated there without applying the new query; the screen now tracks the state hash each fetch reflects and refetches when it has drifted, keeping the scroll-preserving no-op only when nothing changed. -- **An active custom highway renderer is no longer starved of `draw()` when it hides the canvas** (#819). The per-frame draw gate in `static/highway.js` bailed on `if (!_lastVisible) return`, which conflated two different "hidden" states: a genuine off-screen canvas (`offsetParent === null` — navigate-away / `display:none` splitscreen panel, #246) versus a renderer-set *override-hide* (`setVisible(false)`, where an opaque overlay covers the canvas but the active renderer keeps painting its own surface). The gate now only pauses everything for the off-screen case (and still pauses the default 2D renderer on an override-hide); the **active custom renderer** keeps receiving `draw()` through its own override-hide. The `highway:visibility` event still fires before the gate, so sibling overlay renderers (e.g. 3D Highway's `.h3d-wrap`) still pause. This is the core-side root cause behind the Tab View cursor freezing in single-player (slopsmith#734; worked around plugin-side in slopsmith-plugin-tabview#25). -- **Screensaver no longer kicks in during windowed-mode playback** (#686). While a song is playing, `static/app.js` now holds a [Screen Wake Lock](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API) (`navigator.wakeLock.request('screen')`) so the OS display/screensaver stays awake even though only audio + the highway animation are active and the keyboard/mouse are idle. The lock is acquired on `song:play`/`song:resume` and released on `song:pause`/`song:ended`/`song:stop` (kept only while actually playing), and re-acquired on `visibilitychange` when the tab refocuses (the API auto-releases a lock whenever the page is hidden). Both the HTML5 `