mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-26 06:42:32 +00:00
fix/input-setup-drop-label-dedup
53 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
90fb2ee3bc
|
feat(v3): content-dependent playlist covers + custom art (#626)
* feat(v3): content-dependent playlist covers + custom art upload Playlist cards were a tiny 🎵 emoji on an empty square. Now the cover reflects the playlist's contents, and you can override it with a custom image. Cover (in priority order): - custom uploaded cover, else - empty playlist -> the icon - a few songs -> the first song's album art - 4+ songs -> a 2x2 album-art mosaic Backend (server.py): - MetadataDB.list_playlists() returns each playlist's first few still-present songs' art URLs (`art_urls`) for the content cover. - GET /api/playlists and GET /api/playlists/{id} add `cover_url` when a custom cover exists. - POST/GET/DELETE /api/playlists/{id}/cover — store a small PNG thumbnail under CONFIG_DIR/playlist_covers/ (PIL-converted, mirroring song-art upload); the cover is deleted with the playlist. Cover mutators added to _MUTATING_ROUTES. Frontend (static/v3/playlists.js): playlistCoverHtml(p) renders the rules above; the playlist detail view gets "Cover" (pick an image) + "Remove cover". Tests: tests/test_playlists_api.py (art_urls + cover roundtrip / reject-non-image / delete-removes-cover — 11 pass) and tests/js/v3_playlist_cover.test.js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(playlists): 400 (not 500) on non-string cover image + bust same-second cover cache Two review follow-ups on the playlist-cover endpoints: - POST /cover did `if "," in b64` before any type check, so a non-string image (e.g. {"image": 123} / null) raised TypeError -> 500. Guard with isinstance (mirrors the avatar/song-art upload) for a clean 400. +regression test covering number/null/object/list. - The cover URL busted only on int(st_mtime) (1s granularity) and GET /cover sent no cache headers, so a same-second replace/remove/re-upload could serve a stale image. Use st_mtime_ns in the cache-bust token and add the shared no-cache header (_ART_CACHE_HEADERS), matching song art. 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> |
||
|
|
3d97c07b2b
|
feat(v3): add "Add to playlist" to a song's ⋮ More menu (#625)
* feat(v3): add "Add to playlist" to a song's ⋮ More menu You could only add a song to a playlist via select-mode (checkbox → batch bar). Add an "Add to playlist" row to each song card's ⋮ overflow menu that targets that one song, reusing the same picker (pick a listed number or type a new name to create the playlist). The select-mode batch flow and the single-song menu now share one extracted `addFilenamesToPlaylist(filenames)` helper; the menu is `openCardMenu`, shared by grid cards and tree rows, so both views get it. Tests: tests/js/v3_add_to_playlist_menu.test.js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(v3): don't clear the batch selection when the playlist picker is cancelled The extract-helper refactor made batchAddToPlaylist() call finishBatch() unconditionally, so cancelling (or a failed create) cleared the multi-select and reloaded the grid — a regression from the original early-return-on-cancel behaviour. addFilenamesToPlaylist() already returns null on cancel/failure; gate finishBatch() on a truthy playlist id so the selection is preserved for a retry. Adds a regression assertion. 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> |
||
|
|
b103a722ce
|
fix(v3): refresh Songs grid after a Settings rescan / DLC-folder change (#624)
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 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
d841813e0b
|
fix(library): Edit Metadata modal — editable Year + don't close on drag-release outside (#623)
* fix(library): Edit Metadata modal — editable Year + no close on drag-release Two fixes to the Songs -> Edit Metadata modal (openEditModal/saveEditModal in static/app.js), both reported on macOS for 0.3.0. 1) Year is now editable. A year can be set when authoring a pak but the modal had no Year field, so it could never be changed. The backend (POST /api/song/<f>/meta) already accepts + normalizes `year` and writes it into the file via songmeta (survives a rescan) -- only the UI omitted it. Add a Year input (populated from the song's current year) and include `year` in the save POST body. Both the v3 card menu and the legacy edit button already pass the year through, so both surfaces get the field. 2) The 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 edge dismissed the form without warning (the `click` event's target resolves to the backdrop, the common ancestor) -- discarding the edit. Backdrop dismissal now also requires the mousedown to have STARTED on the backdrop, tracked per-modal and decided by a new pure helper _editModalShouldClose(clickTarget, modalEl, downOnBackdrop). Cancel / X still close on a normal click. Tests: tests/js/edit_metadata_modal.test.js extracts the real functions from app.js and asserts (a) openEditModal renders #edit-year, (b) saveEditModal's meta POST body carries `year`, and (c) the backdrop-close decision table (Cancel always closes; backdrop needs down+up on the backdrop; a drag from a field released on the backdrop does NOT close). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(library): wire Edit Metadata Save via listener, not an inline onclick encodeURIComponent does not escape "'", so embedding the filename in the single-quoted inline onclick="saveEditModal('…')" handler produced a malformed handler for any song whose filename contains an apostrophe (e.g. Bob's Song.sloppak) — clicking Save threw a syntax error and the edit silently failed. Replace the inline onclick with a data-edit-save hook wired in JS from the closure filename (mirrors the existing Delete button pattern), so the filename never has to survive attribute-string embedding. Pre-existing bug surfaced during review of this modal. Adds a regression assertion (no inline saveEditModal onclick; Save wired via data-edit-save). 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> |
||
|
|
ee7bafbb47
|
fix(v3): decode stats:recorded filename so post-play score badge refreshes (#620)
PR #574 added a `stats:recorded` -> in-place accuracy-badge repaint so a just-earned score shows without restarting the app. But the repaint never matched a card, so the badge stayed stale until a full render() (app restart / search / re-enter the screen) -- exactly the "only updates after a restart" report. Root cause is a filename key-space mismatch. The event (like song:loading) carries the filename `encodeURIComponent`'d, because that is what playCard hands to playSong (the highway WS decodeURIComponent's it). Library cards, though, key on the DECODED localFilename (data-fn), and /api/stats/best is server-canonicalized to that same decoded key (server.py _canonical_song_filename). So repaintAccuracy's `data-fn !== key` check rejected every card and `state.accuracy[encoded]` was undefined. Decode the event filename back into the card / state.accuracy key space via a small `decFn` helper before marking dirty and repainting, fixing both the immediate repaint and the onV3SongsScreenEnter deferred path. decFn is idempotent for already-decoded names and falls back to the original on malformed input, so a real filename containing a literal '%' is never corrupted. Tests: tests/js/v3_songs_score_badge_refresh.test.js extracts the real decFn from the shipped source and proves the encoded event filename round-trips to the raw card key (incl. spaces and subfolder '/'). Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
fef870047b
|
fix(player): reliable Escape "Back" + resumable, optionally-confirmed song exit (#619)
* fix(player): make Escape a reliable Back; resumable + optionally-confirmed song exit Escape didn't always leave a song: clicking a transport control (play/FF/RW/ restart) left that <button> focused, and _shortcutDispatchBlocked() bails the shortcut dispatcher for any focused INPUT/SELECT/TEXTAREA/BUTTON — so the player-scope Escape=Back shortcut never fired until the user clicked empty canvas to blur the control. Space already had a player-screen carve-out (#593); Escape did not. That asymmetry was the bug. Phase 1 — focus fix: generalize the Space carve-out in _shortcutDispatchBlocked to Escape, on the player AND settings screens (both register Escape=Back; settings had the identical latent bug). The earlier guards still win: text inputs are exempted first, the Section Practice popover already claims Escape, and a true modal (role=dialog aria-modal=true / .feedBack-modal) still traps it. Plugins' player-scope Escape shortcuts are fixed identically. Phase 2 — resume: leaving the player snapshots {song, arrangement, position, speed} to localStorage; a non-blocking "Resume practice" pill offers it back on the next non-player screen / next launch. playSong() gains a {resume} option that restores speed + seeks to the saved position on song:ready instead of the normal autostart. Conservative (ignores <3s / near-end), cleared on natural song-end and once consumed, expires after 24h. Phase 3 — opt-in "Ask before leaving a song" (Gameplay tab, default OFF). A true-modal confirm with monotonic Escape (the second Escape leaves) and Space/Enter = Leave. The player Escape shortcut and the v3 close button route through window.requestExitSong(); auto-exit on song-end and a results screen's own Close stay unguarded. Design rationale: a multi-seat design charrette (engagement, learning-design, operability, codebase-reality) — leaving a song should be reliable and recoverable, not gated; the confirm is opt-in only. Tests: tests/browser/{keyboard-shortcuts,resume-session,exit-confirm}.spec.ts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * test(browser): suppress first-run onboarding in keyboard/resume/exit specs The first-run onboarding overlay (#v3-onboarding) is a modal that intercepts pointer/keyboard events; on a fresh profile it covers the player and breaks any test that presses Escape or clicks. Stub GET /api/profile to an onboarded profile in each beforeEach so the app behaves like a returning user (the state these tests assume). Also tighten the Section Practice Escape test to assert the guarantee the fix actually provides — Escape does not exit the song while the popover is open (the line-447 guard wins over the carve-out) — rather than asserting the popover's own close handler fires, which isn't wired for a synthetic bar. Verified locally against a worktree server (Chromium): all 16 new specs pass (5 Escape + 6 resume + 5 exit-confirm) plus the existing #593 Space tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(player): exit-confirm — Escape cancels back to song, pause on open/resume on stay Refinements from tester feedback on the exit-confirm (default stays OFF): - Escape on the open prompt now = Stay (dismiss + return to the song), matching every other modal and the generic _confirmDialog (Esc=cancel). A second Escape therefore returns to the song instead of leaving it. Leaving stays the explicit, default-focused "Leave" button, so Space/Enter/click = "just get me out" (the OP's "Space always hits leave"). - Opening the prompt PAUSES the song (via the canonical togglePlay path, HTML5 + _juceMode) so it isn't running/being scored behind the modal; Stay resumes exactly what we paused. Guards: cancel any count-in on open; resume only if we paused (wasPlaying), only if still the same live song on the player (_audioSeekGen unchanged), and never auto-resume a song the user had paused. - Trap Tab inside the dialog; backdrop click was already Stay. Specs: exit-confirm.spec.ts updated — the monotonic "second Escape leaves" test becomes "second Escape stays", plus a backdrop-click-stays test. The audio pause/resume itself is verified manually on web + desktop (the mock song has no backing track); these specs lock the navigation + keyboard semantics. NOTE: the pause/resume adds a new pause→resume cycle on the desktop JUCE transport (known play/pause-desync path) — smoke-test on the desktop build before merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(player): accurate exit-confirm copy + keep resume snapshot on failed load Two review follow-ups on the Escape/resume/confirm work: - Settings copy said "a second Escape (or Space/Enter) still leaves", but Escape dismisses the confirm (Stay) like every other modal — only Space/Enter/Leave exit. Corrected the Gameplay-tab description so it matches the implementation (and the committed exit-confirm specs). - resumeLastSession() cleared the snapshot BEFORE awaiting playSong(), so a transient load/connect failure permanently lost the Resume pill with no retry. Clear only after the load resolves; on failure keep the snapshot (and drop the pending in-memory resume) so the pill re-offers it on the next non-player screen. All 16 Escape/resume/exit-confirm Playwright specs still pass. 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> |
||
|
|
b206633131
|
fix(v3): keep Section Map's leftmost section clickable under the rail catcher (#617)
The section_map plugin pins a ~20px clickable bar (#section-map, z-index:5) to the top of #player. The v3 left-rail hover-catcher (.v3-railzone::before) is full-height at z-index:30 with pointer-events:auto, so its top-left corner swallowed every click on the section map's first section — the left-most section was never clickable on the v3 desktop (macOS/Windows) UI. Drop the catcher below the 20px bar when the section map is present, mirroring the existing #section-map ~ #player-hud special-case in static/style.css. The rail still reveals from anywhere below the bar. Adds a Playwright regression test (hit-test of the top-left corner) with a negative control that re-raises the catcher to reproduce the bug. Fixes #616 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3b2d83d406
|
feat(folder_library): Folder Library core plugin (#610)
Adds the bundled Folder Library plugin (browse the DLC library by its on-disk folder tree, in-app folder CRUD, drag-and-drop + dialog song moves, sort/filter, live search), wired into the classic v2 toolbar and the v3 Songs page. Includes the screen.js IIFE dedup (unified surface factory) and review fixes: path-traversal guard on /song/move, folder-delete data-loss fix, plural /api/plugins/<id> namespace, loose-folder song recognition, error-text escaping, v3 setLibView null-guard, and tests. Co-authored-by: Kyle <kyle.j.t@live.co.uk> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6dbcc5861b
|
fix(player): keep play/pause button in sync when a JUCE reroute aborts autoplay's play() (#611)
On the first song after a fresh load on desktop, the audio engine is often still starting when the song loads, so the song begins on the HTML5 <audio> element and the engine-reroute watcher then migrates it to the JUCE backing transport. The reroute's first step is a deliberate audio.pause(), which rejects autoplay's in-flight togglePlay() audio.play() with an AbortError — even though playback continues on JUCE. togglePlay()'s catch then reset isPlaying=false and the button to "Play" while the song kept playing: the button showed Play during playback, so it took two clicks to actually pause (one to resync the flag, one to pause). The reroute already guards the <audio> 'play'/'pause' DOM listeners with window._juceRerouteInProgress; this extends the same guard to togglePlay()'s catch and the count-in catch, so a play() rejection caused by the reroute's own pause doesn't clobber the button. A genuine failure (outside a reroute) still resets correctly. Adds a regression test that drives togglePlay() through a reroute-aborted play() and asserts the button stays Pause; it fails without the guard. Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
8f0625e1f7
|
fix(v3): reset live performance HUD on backward seek / restart (#607)
The v3 live performance HUD (the visible top-right score tracker) keeps
its own hits/misses/streak counters from note:hit / note:miss events and
only reset them on song load / stop / ended — not on a seek. So pressing
Restart (or scrubbing back), which only repositions the playhead and
emits song:seek, left the tracker showing the stale cumulative score
(tester report).
Mirror the notedetect HUD fix: keep a per-note {t,hit} ledger (note:hit/
note:miss carry the judgment incl. noteTime) and, on a BACKWARD song:seek,
rebuild the tally to reflect only the notes up to the new playhead
(Restart -> "Waiting for notes" / 0). Forward seeks keep earlier notes;
loop-wrap (drill mode) is skipped so a practiced A-B loop still
accumulates, matching the notedetect HUD.
Tests: +3 in tests/js/live_performance_hud.test.js (backward rebuild,
restart-to-0, forward no-op, loop-wrap ignored). Existing 10 still pass.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
c8e0ad3f75
|
fix(gp-import): correct bass string count, lead/rhythm roles, preview note count (#601)
Four tester-reported GP-import issues, all in the converter/parse layer: * String count (bugs 2 & 4): <tuning> is padded to 6 slots, so a 4-string bass, 5-string bass and 6-string guitar were byte-identical and the real count was lost — a 5-string bass played on 4 strings and a 4-string bass showed a phantom B in the editor. Record the authoritative count in a new <tuning stringCount=N> attribute (gp2rs._build_xml) and trim the padded tail back to it on read (song.parse_arrangement). All consumers already trust a non-6 tuning length (arrangement_string_count, the editor's _stringCountFor and build-time _normalize_tuning_to_count), so this fixes the create-mode preview AND the built sloppak with no consumer changes. * Lead/Rhythm reversed (bug 3): guitar arrangements were named by appearance order (first guitar -> Lead), swapping roles for files that list Rhythm before Lead. Honor 'lead'/'rhythm' in the GP track name; unhinted tracks keep positional fallback. Applied to both convert_file's fallback (the editor's track_indices-without-names path) and _auto_select_gpx, with cross-role dedup so name-based and positional labels can't collide. * Preview note count (bug 1): the importer's per-track count included tie-continuation notes, which are folded into the previous note's sustain and never become separate RS notes (260 shown vs 241 imported). Exclude tie destinations so the preview matches the imported result. Adds regression tests for all three. Bug 5 (no stems from synced audio) is environment-dependent (best-effort demucs backend) and not addressed here. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4c3ec2ff66
|
feat(plugins): full-screen (immersive) plugin screens via manifest opt-in (#590)
DAW-style plugin UIs (e.g. a practice studio) need the whole viewport, not a scrolling content page below the v3 topbar — embedded in the shell they get cut off at the bottom with excess padding up top. Add an opt-in top-level `"fullscreen": true` plugin.json field, surfaced as the `fullscreen` boolean on /api/plugins (mirrors the settings_category plumbing in plugins/__init__.py). When a fullscreen plugin's screen is active, static/v3/ shell.js toggles `html.fb-immersive` from syncActive() so it tracks every navigation incl. deep-link; static/v3/v3.css then hides the topbar, collapses the sidebar to a functional icon rail (kept reachable — Escape is bound only on player/settings scopes, so a fully hidden sidebar would trap the user), and lets the active plugin screen fill #v3-main. Mirrors the existing ss-follower-pre chrome-hide pattern. Additive + opt-in: plugins without the flag are unaffected. Test: tests/test_plugins.py::test_fullscreen_flag_parsed_from_manifest Claude-Session: https://claude.ai/code/session_01BmWopMsRjdZyD6RwmZAQBv Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
8b4c9b0050
|
Fix list/tree view: select mode, parts visibility, song actions (#585)
* Fix list/tree view: select mode, parts visibility, song actions Bring the v3 list/tree view to parity with the grid card: - Select mode now renders a per-row checkbox + selected-ring, preserves expanded artist groups across re-render, and a capture-phase guard makes a row/chip click select the song instead of starting playback. - Always-on favourite / save-for-later / overflow-menu cluster on each row, same actions as the grid card. Rebuild static/tailwind.min.css so the new utilities are compiled in - notably .sm:flex behind the arrangement chips' "hidden sm:flex" wrapper. Without it the chips (and #582's badges) render display:none on the Docker build, which serves the committed CSS; the desktop build looked fine only because it rebuilds Tailwind from source at bundle time. Signed-off-by: Sin <deathlysin@outlook.com> * fix(v3): regenerate tailwind.min.css from source + add tree select tests + CHANGELOG The committed tailwind.min.css was over-built: 135,578 bytes / 1,428 selectors, with 294 selectors (accent-amber-400, bg-cyan-500, animate-spin, after:bg-gray-400, …) used in zero core source files — bloat from a local build scanning outside the repo's content globs. It would fail CI's rebuild-and-diff and violates the byte-stable rule in scripts/build-tailwind.sh. Regenerate via `scripts/build-tailwind.sh` (pinned tailwindcss@3.4.19): 111,491 bytes / 1,134 selectors, byte-identical to a clean rebuild, still containing the .sm\:flex fix plus every new tree class (ring-fb-primary, accent-fb-primary, pointer-events-none, …). Docker chips now render and CI stays green. Add tests/browser/v3-tree-select.spec.ts: - select mode keeps expanded artist groups open across the tree re-render (fails without loadTree's openArtists capture/restore) - clicking a row in select mode selects instead of playing Record the fix under CHANGELOG [Unreleased] -> Fixed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Sin <deathlysin@outlook.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
82db8e56b1
|
chore(hotkeys): remove sloppak-convert library hotkey (#594)
* chore(hotkeys): remove sloppak-convert library hotkey
Removes the 'c' keyboard shortcut for converting library entries to
.sloppak. The shortcut was defined in two places:
- The no-op registerShortcut() entry that only existed to show in the
? help panel (the Sloppak Converter plugin handles conversion and
can register its own shortcut via window.registerShortcut).
- The c dispatch in the library-entry keydown handler
({ c: 'button.sloppak-convert-btn', ... }) that triggered the
plugin button.
* test+docs: update tests & CHANGELOG for removed `c` convert hotkey
The previous commit removed the `c` library hotkey but left three
assertions in tests/browser/keyboard-shortcuts.spec.ts that require it,
which fail deterministically (the two registry tests read window._panels
directly, independent of environment):
- should list all registered shortcuts (required {key:'c',scope:'library'})
- should have correct shortcut scopes (expected library::c)
- should show library shortcuts in help modal (Convert library entry / c)
Drop those assertions and record the removal under CHANGELOG
[Unreleased] -> Removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: byrongamatos <xasiklas@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
64801d5735
|
fix(player): Space bar play/pause when focus is on sidebar or rail buttons (#593)
* fix(player): Space bar play/pause when focus is on sidebar or rail buttons When any <button> in the player rail (viz, audio, mixer, etc.), a sidebar nav link, or a popover control has keyboard focus, pressing Space was blocked by _shortcutDispatchBlocked → _isInsideInteractiveControl, which returns true for BUTTON elements. The Space shortcut never reached the shortcut dispatcher and togglePlay() was never called. The fix extends the same carve-out pattern already used for the section practice bar: when the player screen is active, Space is always dispatched through the shortcut system. The shortcut handler's preventDefault() stops the focused element from also activating, so this is not a double-trigger. * test(player): cover Space play/pause carve-out + add CHANGELOG entry Adds two Playwright regression tests for #593 in tests/browser/keyboard-shortcuts.spec.ts: - Space toggles play/pause when a player rail <button> has focus, and the focused button does NOT also activate (dispatcher preventDefault). Fails on base (Space blocked, played=0), passes with the carve-out. - Space in a player-screen text input still types a space and never reaches play/pause (locks the _isTextInput exemption ordering). Also records the fix under CHANGELOG [Unreleased] -> Fixed, per the project workflow that every PR updates the changelog. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(player): don't override Space inside modal dialogs over the player The player-screen Space carve-out keyed off the active *screen*, so it also hijacked Space inside a true modal dialog layered over the player (e.g. the keyboard-shortcuts help modal, edit modal): Space toggled playback behind the modal and preventDefault blocked the modal's focused control (Close) from activating — contradicting aria-modal semantics. Narrow the carve-out to skip focus inside a modal (role="dialog" aria-modal="true" or .feedBack-modal). Non-modal player popovers/toasts (loop A/B, arrangement pin, role=dialog aria-modal=false) are not dialogs and stay covered, so the original fix is unchanged for the cases it targeted. Adds a Playwright regression test (Space inside a modal reaches the modal's button, not play/pause) and updates the CHANGELOG entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: byrongamatos <xasiklas@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d2569cc2a8
|
feat(achievements): wall sync drain worker + review fixes (epic PR3) (#592)
* feat(achievements): wall sync drain worker (epic PR3, client side) Background dead-letter worker that POSTs queued Feat unlocks/removals to the hosted feedback-achievements wall. Idle unless FEEDBACK_ACHIEVEMENTS_WALL_URL is set; uses requests + the client-token header (mirrors lyrics_transcribe). Dead-letter, never drop (pure engine.drain_decision): network err / 429 / 5xx -> keep pending (retry) other 4xx -> dead_letter (diagnosable, replayable) 2xx -> delete on server ack remove-me enqueues a wall removal keyed by the reused player_hash. Verified by an end-to-end staging round-trip (earn a Feat -> drains onto the wall with name + short hash -> remove-me -> wall empties) with no IP in tables or access logs. 42 plugin tests pass (test_sync.py adds the decision table + ack/retry/dead-letter retention + four-field on-the-wire payload). The hosted service lives in the new feedback-achievements repo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(achievements): address local review findings (epic) Bugs caught in the pre-merge review loop: - secret_witching Feat was DEAD: post_activity wrote witching_nights_run to the DB before snapshotting prev_tiers, so diff_unlocks never saw the fresh unlock. Fold the run into the activity delta instead (same asymmetry chart_encore uses) so the 7th-night unlock is detected. +regression tests. - chart_encore broke across restarts: per-chart counter keyed on abs(hash(str)), which Python salts per-process (PYTHONHASHSEED). Use a stable sha1 digest so the same chart accumulates across sessions. +regression test. - Bounded the per-activity counter read: _read_counters no longer pulls the unbounded chart_plays:* rows (they're bumped/read individually). - screen.js: gate note:hit/miss on an active-song flag so tuner/calibration note events can't inflate Feats or flush a phantom chart:null session. - screen.js: P-III — prefix the plugin localStorage key (achievements:profile-cat). - screen.js: extract the duplicated local-ISO-date helper. 45 plugin tests pass (3 new). Wall-side review fixes are in the feedback-achievements repo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(achievements): default the drain worker to the hosted wall Point FEEDBACK_ACHIEVEMENTS_WALL_URL's default at the live got-feedback wall (https://feedback-achievements.onrender.com) so the drain worker targets it out of the box; still env-overridable for self-hosting/staging. Nothing publishes unless the user opted in AND has a profile identity, so a default URL alone sends nothing. Tests disable the default (autouse fixture) so no test ever POSTs to production; drain logic is covered via _drain_once() with an injected poster. 45 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
287c23a532
|
feat(achievements): opt-in, privacy controls & data-min gate (epic PR2) (#591)
Sharing earned Feats on the (forthcoming) public wall is strictly opt-in,
default OFF, with a binding data-minimization contract.
- Onboarding (static/v3/profile.js): a new opt-in step (now a 5-step wizard)
after song-directory / before paths — publishes only display name + earned
Feats, never songs/skills/scores; off by default.
- Settings (plugins/achievements/settings.html, System tab via
settings.category): the same toggle + a "Remove me from the wall" button
(POST remove-me — wipes local synced state offline + enqueues removal).
- Core (server.py): achievements_enabled (bool, default false) in
_default_settings + /api/settings validation + _RESETTABLE_SETTINGS_KEYS;
mirrored to localStorage in app.js loadSettings().
- Data-minimization gate: engine.build_wall_payload is the single explicit-dict
serializer; key-set is EXACTLY {display_name, player_hash, achievement_id,
unlocked_at}, achievement_id always a Feat id. Enqueue is gated on
opted-in AND profile identity (reused player_hash); competency never
enqueues (integration law).
Verified natively: settings round-trip + validation + remove-me; opted-in
activity enqueues exactly one 4-field Feat payload; Playwright confirms the
5-step wizard + opt-in card (default unchecked), zero console errors.
29 plugin tests + new settings tests pass.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
05dd3d227a
|
feat(achievements): local engine + tabbed Profile shell (epic PR1) (#587)
Adds the Achievements & Feats of Power local engine, fully offline. Core (static/v3/profile.js): the Profile screen becomes tabbed exactly like v3 Settings (.fb-tabbar/.fb-tab/.fb-tabpanel, active tab persisted in localStorage 'v3-profile-tab'). A Profile (main) tab carries the existing cards + a Feats trophy-shelf mount (#v3-profile-feats-slot, earned-only), and an Achievements tab carries a plugin mount (#v3-profile-achievements-mount) + empty-state note. A new `v3:profile-rendered` event fires after every render so the plugin re-injects (mirrors v3:settings-rendered). New bundled plugin (plugins/achievements/): SQLite engine (unlocks/counters/comp_ledger/sync_queue) with pure threshold/criterion math in the testable sibling engine.py (P-V); routes activity/ report-unlock/report-criterion/catalog/earned/feats/remove-me. Feats read activity counters only (batched song:ended POST; notes only when notedetect present — graceful degradation); competency Achievements evaluate from progression events only — the integration law, never crossed. Catalogue is always shown (locked=greyed), grouped by the real progression paths (Global/Guitar/Bass/Drums/Keys, auto-extending) with per-category earned badges. Versioned window.feedBack.achievements registration API with the __feedBackAchievementsPending load-order queue + achievements:ready event. Verified natively (uvicorn) end-to-end + Playwright (tabbar, earned-only Feats shelf, greyed catalogue, registration API, zero console errors); 24 plugin tests pass incl. the integration-law assertion. Opt-in/privacy/data-min gate (PR2) and the hosted wall (PR3) follow. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3b485fe62b
|
feat(v3): tabbed, card-row settings page + per-plugin settings category (#584)
Replace the single long scrolling v3 settings screen with a horizontal tab bar (Gameplay / Audio / Graphics / Keybinds / Progression / Mic / Plugins / System) over card rows (icon + title + description, control on the right) with a per-category Reset. - static/v3/index.html: tab bar + card-row markup (ids keep hydrating through the unchanged app.js loadSettings()/persistSetting() path). - static/v3/settings.js (new): tab switching + active-tab persistence (localStorage 'v3-settings-tab'), per-category reset, read-only Keybinds reference from window.getAllShortcuts(). - static/v3/v3.css: plain CSS, no Tailwind rebuild. - Per-plugin settings tab: new optional settings.category in plugin.json → plugins/__init__.py surfaces settings_category; app.js mounts each plugin <details> into #plugin-settings-<category> (fallback: Plugins tab). highway_3d ships category: "graphics". - New gameplay settings: countdown_before_song (wired end-to-end, default off); miss_penalty + fail_behavior (persist-only stubs); "Note highway speed" surfaces existing master_difficulty. - New POST /api/settings/reset clears whitelisted keys back to defaults. Tests: test_settings_api.py, test_plugins.py::test_settings_category_parsed_from_manifest, tests/browser/settings-tabbed.spec.ts. 179 passed locally. Ported from the pre-rename feat/v3-settings-tabbed WIP onto current main (slopsmith→feedBack rename applied; settings-screen markup conflict resolved in favour of the new tabbed layout — all prior setting ids preserved). Closes #579 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f3a5cb9ed3
|
feat(sloppak): expose full-mix original_audio alongside stems (#583)
Lets a .sloppak ship the single pre-separation full mixdown next to its
per-instrument stems, so the player can use the pristine original when nothing
is isolated (demucs recombination is lossy) and switch to separated stems only
when a slider drops below unity.
- lib/sloppak.py::load_song parses the optional manifest `original_audio:` key
into a new LoadedSloppak.original_audio field, with the same path-traversal
guard + permissive "missing → disabled" posture as the drum_tab loader.
- The highway WS song_info frame additively carries original_audio_url (served
by the existing /api/sloppak/{filename}/file/{rel_path} endpoint, None for
stems-only packs), has_original_audio, and has_stems.
- A stem-less, full-mix-only sloppak now sets audio_url to the full mix (plays
natively) instead of emitting audio_error.
Message shape stays a stable contract — all additions are purely additive.
Tests: tests/test_sloppak_original_audio_load.py (6 passing).
Closes #580
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
af2949677a
|
rename: slopsmith → feedBack, byron → got-feedBack (#537)
* 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: byrongamatos <xasiklas@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
37aedd4251
|
Restore GP6/7/8 tremolo picking on import (#572)
* Map GP7/8 tremolo picking on import GP7/8 (GPIF) encodes tremolo picking as a beat-level <Tremolo> element, which the importer ignored — so tremolo was silently dropped on .gp import, while note vibrato and the GP3-5 path were unaffected. Read the beat-level <Tremolo> and set the note tremolo flag across the beat, independent of vibrato (a note can carry both). Signed-off-by: Sin <deathlysin@outlook.com> * test: cover GP6/7/8 tremolo-picking import Extract the beat-level <Tremolo> detection into a pure _beat_has_tremolo helper (mirroring the tested _note_has_vibrato) so it's unit-testable in this suite's fixture-free style, then add: - 4 unit tests on _beat_has_tremolo: direct <Tremolo> child detected (rate-agnostic), absent -> False, direct-child-only (nested Tremolo ignored), independent of the VibratoWTremBar whammy property. - 1 end-to-end test driving convert_file via a crafted GPIF (monkeypatched _load_gpif): a tremolo-picked beat's note serializes tremolo="1" while a plain beat stays "0". Both the detection and integration tests fail without the fix; full GP suite 238 passed. Refactor is behavior-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Sin <deathlysin@outlook.com> Co-authored-by: Byron Gamatos <xasiklas@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
32127bc70b
|
feat: save as .feedpak; discover and load both .feedpak and .sloppak (#553)
* feat: save songs as .feedpak; discover and load both .feedpak and .sloppak The open song format was renamed sloppak -> feedpak (public spec lives in the feedback-feedpak-spec repo), but the server still wrote and recognized only `.sloppak`. The two are byte-identical on disk. Read both suffixes everywhere songs are discovered, uploaded, and loaded; writing the new `.feedpak` suffix is handled in the editor plugin repo. Keep the internal `format` tag `sloppak` so existing feature gates (stems, drums, keys) are untouched, matching the "internal rename not landed yet" stance. - lib/sloppak.py: add FEEDPAK_EXT / SLOPPAK_EXT / SONG_EXTS; is_sloppak() now matches either suffix (covers all 7 callers). - server.py: union scan glob over SONG_EXTS; widen loose-folder exclusion, settings DLC count, upload gate (_ALLOWED_SONG_EXTS) and zip-magic check; refresh user-facing messages to .feedpak. - static: library format filter relabeled Sloppak -> Feedpak (value stays sloppak, matches both); badge text SLOPPAK -> FEEDPAK in v2 + v3; filename-suffix detection and upload drag-drop filter accept both. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: topkoa <topkoa@gmail.com> * test: cover .feedpak/.sloppak dual-suffix support Add tests/test_feedpak_extension.py pinning the four paths PR #553 widened so a refactor can't drop .sloppak back-compat or stop accepting .feedpak: - is_sloppak / SONG_EXTS suffix detection (file + dir form, case-insensitive) - _background_scan discovery glob unions over both suffixes - POST /api/songs/upload accepts both, rejects wrong suffix + non-zip - save_settings DLC count includes both suffixes 19 tests, all passing; reuses the existing scan_module / TestClient / isolate_logging fixtures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: topkoa <topkoa@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
187d0bb978
|
fix(highway): stop stale viz frame bleeding through after switching visualizations (#565)
Switching between the 3D drum highway (renders onto #highway) and the 3D guitar highway (renders into its own .h3d-wrap overlay) left the previous drum frame showing through the gap the overlay did not cover. Core: _setRenderer now replaces #highway on a genuine viz change (keyed on viz id via _rendererVizKey, not object identity, so benign same-viz re-installs don't churn the canvas) as well as on a context-type change. highway_3d: applySize pins the .h3d-wrap overlay to #highway's exact box, derived from the same getBoundingClientRect measurements that size the renderer (sub-pixel correct under zoom). Re-pins once the canvas lays out (init race) and resets to the static anchor in the not-laid-out fallback. Reviewed locally via codex (5 rounds, converged clean). CI checks are the known org Actions billing block, not real failures. |
||
|
|
5145710a8a
|
fix(stats): decode song_stats filenames so "Your best scores" reads real data (#564)
The stats-recorder relays URL-encoded filenames (encodeURIComponent: '/'→'%2F', ' '→'%20') and POST /api/stats stored them verbatim, but the `songs` table — and every stats read that filters on `filename IN (SELECT filename FROM songs)` — keys on the decoded library path. So recorded plays landed under a non-matching key and were dropped by the filter: the profile "Your best scores" panel, the library accuracy badges (/api/stats/best) and "Jump back in" (/api/stats/recent) all read empty despite real history. PR #549/#550 wired the panel correctly; this fixes the data layer underneath it. - Canonicalize the filename to its decoded form on the write path (_decode_song_filename in api_record_stats). This also lets the arrangement-count bound resolve the real song. - One-time idempotent backfill (_migrate_decode_stat_filenames) that decodes existing rows, merging PK collisions with best=max / plays=sum / last-wins semantics. - Regression tests: encoded write surfaces in top/best/recent + per-song read; arrangement bound still applies; migration decodes + merges legacy rows and is idempotent. Verified against a copy of a real profile DB: top_stats went 0→5 rows, best-accuracy map 0→12, zero encoded ghosts left. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fe8d30ce3e
|
fix(highway): apply 3D fret-spacing live instead of reloading (#561) (#562)
window.h3dSetFretSpacing was the only 3D-highway setting that applied via location.reload(). The SPA boots with #home as the active screen and has no restore-last-screen mechanism, so the reload ejected the user from Settings onto the home screen. Apply it live like every other 3D-highway setting: rebind the module-scope _h3dFretUniform flag (so panels mounted later this session pick up the new mode), recompute the two fretX-derived scalars baked at init (_fretLabelScaleRefW, FRET_WIDTH_MID), and broadcast 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. Settings copy updated (no longer reloads) and tests/js pin the no-reload / live-rebuild behavior. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4dc5936712
|
feat(player): global autoplay & auto-exit option (songs + lessons) (#558)
* fix(v3): pedal click opens the plugin's screen, not its settings The v3 Pedalboard's settingsTarget() resolved settings-first, so a plugin that ships both a screen and a settings panel (notably the bundled Audio Engine) could only ever reach its settings from the pedalboard — its actual page was unreachable. Flip to screen-first (stompbox metaphor: step on the pedal, see the pedal), falling back to settings when there is no screen. Keep a settings fallback in openPluginSettings() when a declared screen isn't mounted yet (installing/failed) so settings-bearing plugins are never stranded on a toast. Drive the pedal aria-label off the same target so it never promises the wrong surface. Update the unit test contract to screen > settings > none. Fixes #555 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(player): global autoplay & auto-exit option (songs + lessons) Single Settings toggle (autoplayExit, default ON) that auto-starts a song once it's ready and returns to the launching menu when it ends. Auto-exit defers while a results/score overlay is on top (heuristic + holdAutoExit() contract) so a scoring plugin's screen drives the exit. Player origin is now context-aware (lessons return to the lessons screen via setReturnScreen()), fixing lesson completion bouncing to the library. Core-only; songs and lessons share the playSong -> highway path. Adds a read-only window.slopsmith.autoplayExit getter + holdAutoExit()/setReturnScreen() for plugins. Unit tests for the pure helpers (_autoplayExitEnabled, _resolvePlayerOrigin, _resultsOverlayVisible). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f79efe2516
|
fix(v3): pedal click opens the plugin's screen, not its settings (#556)
The v3 Pedalboard's settingsTarget() resolved settings-first, so a plugin that ships both a screen and a settings panel (notably the bundled Audio Engine) could only ever reach its settings from the pedalboard — its actual page was unreachable. Flip to screen-first (stompbox metaphor: step on the pedal, see the pedal), falling back to settings when there is no screen. Keep a settings fallback in openPluginSettings() when a declared screen isn't mounted yet (installing/failed) so settings-bearing plugins are never stranded on a toast. Drive the pedal aria-label off the same target so it never promises the wrong surface. Update the unit test contract to screen > settings > none. Fixes #555 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
63eb7a4ffc
|
feat(progression): fancy notifications for quest/path progress + completion (#552)
* feat(progression): fancy notifications for quest/path progress + completion (#551) Surface achievement feedback as in-app toasts when the player advances or finishes a daily/weekly quest, and when they progress or level up an instrument path. - progression-core.js: _diff() now emits two partial-advance events — quest-progressed (a still-incomplete quest whose count rose) and path-progressed (a challenge toward the next level completed without a level-up). Both are guarded so the increment that COMPLETES a quest / the level-up itself stays a single quest-completed / path-level-up event (no double toast). Period rollovers and brand-new quest ids emit nothing. New events added to the capability owner's declared events list. - notifications.js (new): reusable window.fbNotify toast surface (stacked, animated, auto-dismiss; animation + accent via inline styles so no new Tailwind utilities) + progression wiring — subtle toasts for advances, celebratory toasts for quest completion, path level-up, and rank-up. - index.html: load notifications.js after progression-core. - tests: progression_progress_events (diff emission + guards) and progression_notifications (toast rendering + wiring) — 11 cases. No backend change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(progression): unwrap CustomEvent .detail in notification handlers Codex P2: window.slopsmith.on delivers a CustomEvent (bus.on → addEventListener), so the progression payload is e.detail — not the raw argument. All five notifications.js handlers read the arg directly, so in the browser every field was undefined (e.g. rank-changed never toasted). Unwrap e.detail in each handler, matching every other sm.on consumer. The test harness masked this by invoking handlers with raw payloads; it now wraps them as {detail: payload} like the real bus, so the unwrap is actually exercised (the tests fail without the fix). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a0867f8bfd
|
fix(profile): wire "Your best scores" panel to real song stats (#549) (#550)
The profile card's "Your best scores" panel was a hardcoded placeholder
(`#v3-profile-bests` was never filled), so it always read "Play a song to
start tracking..." regardless of how many songs had been scored. The
backend already records best_score/best_accuracy per song; only this
panel was left unwired.
- server.py: add MetadataDB.top_stats(limit) (per-song aggregate, best
score first, scored songs only, dead songs skipped) + /api/stats/top
route that enriches rows with title/artist/art, mirroring
/api/stats/recent. Declared before the /api/stats/{filename} catch-all.
- static/v3/profile.js: renderBests() fetches /api/stats/top and fills the
panel (rank, title/artist, best accuracy %, score; click to play),
keeping the placeholder only when nothing's been scored.
- tests: cover ordering, per-song aggregation, limit, and
resume-only/dead-song exclusion.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
73e3fe2226
|
fix(song): sanitize caged + guideTones on emit, not just decode (#544 follow-up) (#547)
Post-merge Codex review of #544 found chord_template_to_wire emitted ct.caged and ct.guide_tones raw — so a directly-constructed ChordTemplate(caged="X") or guide_tones=[99] would write a schema-invalid value to the feedpak wire, even though the decoder guards on input. The spec constrains caged to C/A/G/E/D and guideTones to 0..11. Run the same _sanitize_caged / _sanitize_guide_tones guards on emit: caged is written only when a valid enum value, guideTones only as the in-range ints (empty result -> key omitted). +1 test (invalid caged dropped, mixed guideTones filtered to the valid in-range subset, wholly-invalid list omitted). Codex-reviewed: clean. 154 song tests pass. Part of got-feedback/feedback#334. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e518910baa
|
feat(highway): render caged + guideTones teaching labels (§6.6) (#545)
Mirror the voicing/fn.rn teaching-mark render for the two new chord-template
fields, in both the 2D and 3D highways:
- Extend the shared pure chordHarmonyLabels() helper (identical in static/highway.js
and plugins/highway_3d/screen.js) to also surface caged ("CAGED: E") and
guideTones ("gt 4,10"), pre-formatted and node-testable. Invalid caged enum and
out-of-range / non-int guide tones are filtered out.
- Draw both, stacked above the existing rn/voicing labels, in distinct colors.
- Gated behind the SAME teaching-marks toggle (_showTeachingMarks 2D /
teachingMarksVisible 3D) — no clutter on the default highway.
Render only — no scoring / NoteVerifier coupling.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
4195b73877
|
feat(song): wire caged + guideTones chord-template fields (§6.6) (#544)
Mirror the voicing field for the two deferred FEP #24 harmony annotations on ChordTemplate: - caged: str ("C"/"A"/"G"/"E"/"D", "" = unset) - guideTones: list[int] (semitone offsets 0..11 above the root, [] = unset) Both are default-omitted on the wire and sanitized on decode (caged enum-guarded, guideTones filtered to in-range ints, rejecting bool) so a malformed value can't round-trip. GP import is untouched — GP carries no CAGED / guide-tone data. Teaching annotations only; never fed to a grader. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3fc077cbc1
|
feat(highway): render chord harmony fn.rn + voicing on 2D + 3D (§6.3.1, §6.6) (#541)
* feat(core): carry chord harmony fn + template voicing on the wire (§6.3.1, §6.6)
Add two OPTIONAL per-chord harmony annotations (feedpak 1.7.0), mirroring the
teaching-marks (fg/ch/sd) wire work:
- Chord.fn (instance): {rn, q, deg} harmonic-function object, key-dependent.
Validated by _validate_fn on BOTH decode and emit so a partial / out-of-range
fn (which would fail the schema's required-keys rule) never rides the wire.
Default-omitted, mirroring bend bnv.
- ChordTemplate.voicing (template): key-independent voicing-type string
("open", "triad", "shell", "drop2", "barre", ...). Emitted only when
non-empty; non-string wire values fall back to "".
Display/teaching only — never fed to a grader (honesty rule). fn auto-derivation
is DEFERRED (carry-only): a complete rn/q needs chord-quality analysis, and a
deg-only fn would be schema-invalid, so server.py carries author-provided fn
unchanged. GP import unchanged (no reliable per-chord function/voicing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(highway): render chord harmony fn.rn + voicing on 2D + 3D (§6.3.1, §6.6)
Draw the chord's harmonic-function Roman numeral (instance fn.rn) and its
template voicing string, stacked above the chord name on both highways. A shared
pure helper chordHarmonyLabels(fn, voicing) formats the two labels (empty when
absent/malformed) and is node-tested against both files.
Both labels are gated behind the EXISTING teaching-marks opt-in
(_showTeachingMarks / teachingMarksVisible bundle flag) — they're chord-level
teaching overlays, same class as sd/ch, so they stay off the default highway.
2D guards the empty-note-chord case; 3D reuses the gold chord-label sprite style.
Render only — no scoring / NoteVerifier path is touched (honesty rule).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
ea22791984
|
feat(core): carry chord harmony fn + template voicing on the wire (§6.3.1, §6.6) (#540)
Add two OPTIONAL per-chord harmony annotations (feedpak 1.7.0), mirroring the
teaching-marks (fg/ch/sd) wire work:
- Chord.fn (instance): {rn, q, deg} harmonic-function object, key-dependent.
Validated by _validate_fn on BOTH decode and emit so a partial / out-of-range
fn (which would fail the schema's required-keys rule) never rides the wire.
Default-omitted, mirroring bend bnv.
- ChordTemplate.voicing (template): key-independent voicing-type string
("open", "triad", "shell", "drop2", "barre", ...). Emitted only when
non-empty; non-string wire values fall back to "".
Display/teaching only — never fed to a grader (honesty rule). fn auto-derivation
is DEFERRED (carry-only): a complete rn/q needs chord-quality analysis, and a
deg-only fn would be schema-invalid, so server.py carries author-provided fn
unchanged. GP import unchanged (no reliable per-chord function/voicing).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
0f1006972b
|
feat(highway): render teaching marks fg/ch/sd on 2D + 3D (§6.2.2) (#538)
Render the three per-note teaching marks on both highways, mirroring the bend-curve render (#532). Display only — no scoring / NoteVerifier coupling. - 2D static/highway.js: fg renders by default as a small finger numeral hugging the gem (T = thumb, 1..4); sd (degree label) and ch (strum bracket connecting notes that share a ch key, arrow direction from pkd) are opt-in behind a new `showTeachingMarks` toggle (exposed via toggle/get/set + the bundle's `teachingMarksVisible` flag). Pure helpers teachingFingerLabel / teachingDegreeLabel / strumGroupBuckets drive the glyphs. ch bracket is note-stream-only (chord notes already read as one gesture). - 3D plugins/highway_3d/screen.js: fg (default) + sd (opt-in, mirrors the 2D toggle via bundle.teachingMarksVisible) render next to the per-note fret label via a new pooled sprite (pTeachMarkLbl); _scrChordNote resets fg/sd so chord notes don't inherit stale marks. ch strum brackets are deferred in 3D (no cross-note batch pass in the per-note render); 2D covers ch. Tests: tests/js/highway_teaching_marks.test.js extracts the pure helpers from both files (extract-and-eval) and asserts label mapping + strum-group bucketing. Part of got-feedback/feedback#334 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6ee5da3d8b
|
feat(core): teaching marks fg/ch/sd — wire + GP import + sd derivation (§6.2.2) (#536)
Add the three OPTIONAL per-note feedpak 1.5.0 teaching marks — fg (fret-hand finger), ch (strum-group key), sd (scale degree) — to the Note model and wire format, mirroring the bend-shape work (#531). These are DISPLAY/TEACHING ONLY: nothing in the scoring / note-verification path reads them. - lib/song.py: Note.fret_finger / strum_group / scale_degree, default-omitted on the wire (fg/ch/sd) and decoded via _wire_int_optional; _parse_note reads the GP-written fretFinger XML attr. Pure helpers key_to_tonic_pc (§7.7 key name -> tonic pitch class) + scale_degree_for_pitch, plus base_open_string_midis / pitch_from_base / note_pitch_midi (tuning offsets + capo + fret -> MIDI, mirroring app.js _TUNING_BASE_MIDI). - lib/gp2rs.py: GP5 note.effect.leftHandFinger -> fg (RsNote field + fretFinger XML attr), reusing the chord Fingering value convention. - lib/gp2rs_gpx.py: GP8/GPIF per-note <LeftFingering> (p-i-m-a-c letter codes, verified against real GP8 exports) -> fg. - server.py highway_ws: derive sd for notes + chord notes from the active keys.json key + sounding pitch when the author didn't author one (author value wins); base hoisted out of the per-note loop. Tests: round-trip + omit-when-default + malformed-tolerance for fg/ch/sd; key_to_tonic_pc + scale_degree_for_pitch + note_pitch_midi (standard/drop-D/ capo/bass) units; GP5 leftHandFinger and GP8 <LeftFingering> import. Part of got-feedback/feedback#334 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a858617d71
|
fix(bend): GP8 short-bend curve loss + 2D curve timing + 3D bnv gating (#535)
Post-merge Codex review of the bend-curve PRs (#531/#532) surfaced edge cases: - GP8 (#531 P2): bnv timing used rn.sustain, which is zeroed for notes <= 0.2s, so short GP8 bends kept the scalar bn but lost bt/bnv. Use the beat duration `dur` (matching the GP5 path) so the curve survives. - 2D highway (#532 P2): bnvNormalizedPoints mapped x over the curve's own t-range [first,last] instead of the note span, so curves not starting at 0 / ending at sus were time-distorted. Now maps over [0, sus] (clamped), with a curve-span fallback when sus<=0 (existing no-sus callers unaffected). - 3D highway (#532 P3): the sustain ribbon + bend chevron were gated on bn>0, so a note carrying an authoritative bnv with bn==0 drew no ribbon/marker. Both now also fire on bnv presence; chevron steps derived from max(bn, bnv peak). Codex-reviewed: clean (no findings). +1 JS test (sus-relative mapping + fallback). JS 8/8, 250 core GP/song tests pass. NB: GP8's short-bend path still lacks a dedicated synthetic-GPIF fixture (same gap as the GP8 offset-prop-names P3) — _gpx_bend_shape units cover the function; the fix is the one-line caller change. Part of got-feedback/feedback#334. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
351b273ab5
|
feat(highway): render per-note bend curve (bnv) on 2D + 3D (#532)
PR-B of the bend-shape feature (feedpak §6.2.1). Both highways drew a bend
from the scalar `bn` only; now they trace the authoritative `bnv` curve
([{t, v}]) when present and fall back to the `bn` arc/envelope otherwise.
2D (static/highway.js drawNote): when a note carries `bnv`, draw the real
shape as a contour above the gem (round-trip rises then falls, pre-bend
starts high, release descends — `bt` is implicit in the point shape), with
an arrowhead only when the gesture ends rising. `bnvNormalizedPoints` maps
{t,v} to a 0..1 x span. The scalar-arrow path is preserved unchanged as the
fallback; the peak label is unchanged.
3D (plugins/highway_3d/screen.js): `bnvSampleAt` linearly interpolates the
curve (clamped to its endpoints) and `bendSemisAtTime` samples it when
present, else keeps the synthetic rise→hold→release envelope from `bn`. The
chevron count still comes from the peak. Fixed a stale-scratch hazard: the
reused `_scrChordNote` now resets `bnv`/`bt` (omit-when-default) after
Object.assign, mirroring the existing `fhm` reset, so a chord note without a
curve can't inherit the previous note's contour.
Render-only — no wire/schema change. Pure helpers covered by
tests/js/highway_bend_curve.test.js (interp, clamping, round-trip,
degenerate/empty); node --check passes on both files; full tests/js green.
Part of got-feedback/feedback#334
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
e33df9a720
|
feat(core): per-note bend shape (bt + bnv) — wire + GP import (#531)
Implements feedpak spec §6.2.1 (feedpak 1.4.0) per-note bend shape on the
core side:
- `bn` stays the bend's peak magnitude in semitones (unchanged).
- `bt` — bend intent (0 up, 1 release, 2 pre-bend, 3 pre-bend-release,
4 round-trip), default 0, default-omitted on the wire.
- `bnv` — time-stamped bend curve [{t: seconds-from-onset, v: semitones}],
authoritative when present; default-omitted. Older readers ignore both.
Wire (lib/song.py): Note.bend_intent/bend_values; note_to_wire emits bt/bnv
only when set; note_from_wire reads them via _sanitize_bend_curve (drops
malformed entries, empty -> None never []). _parse_note reads them from the
GP-import XML (bendIntent attr + bendValues JSON) so GP curves survive
import -> XML -> wire -> highway.
GP5 (lib/gp2rs.py): _gp_bend_shape maps pyguitarpro BendPoints to a bnv
curve — semitones = value/2.0 (consistent with the existing scalar bn),
t = position/12 * duration — and _bend_intent_from_values derives bt from
the shape. Emitted for <note> and <chordNote> via the shared _build_xml.
GP8 (lib/gp2rs_gpx.py): _gpx_bend_shape builds a 3-point curve from the
GPIF origin/middle/destination value+offset Properties (value/divisor
semitones, offset/100 * sustain seconds), reusing the shared _build_xml.
GPIF offset Property names should be confirmed against a real GP8 export.
Tests cover wire round-trip + default-omit + sanitization, GP5 unit/time
mapping + intent classification end-to-end through the XML, and the GP8
curve builder.
Part of got-feedback/feedback#334
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
b8382139ca
|
feat(core): adopt feedpak_version — read on load + stamp on manifest writes (spec §4) (#530)
Core never read or emitted the manifest `feedpak_version` field. Adopt it:
- sloppak.py: `FEEDPAK_VERSION = "1.2.0"` constant (the format version this build
targets); `LoadedSloppak.feedpak_version` read from the manifest on load
(string, else None for legacy/absent).
- Stamp the version on the two core manifest-rewrite paths, without downgrading
an existing (possibly higher) declared version:
- gp2notation: `setdefault` before its notation-add rewrite.
- songmeta: opportunistically when a metadata field is supplied (gated on the
existing `dirty` flag, so never a standalone rewrite).
Core has no create-from-scratch path (RS-free repo) — the editor plugin's
create-mode save stamping FEEDPAK_VERSION is a follow-up in that repo. Internal
"sloppak" naming is intentionally left as-is (a rename is out of scope / risky).
Codex-reviewed: no P1/P2. +6 tests (read present/absent/non-string; metadata-write
stamp-when-absent / preserve-existing / no-op-no-stamp) + updated the gp2notation
key-order test for the appended version. 197 sloppak/songmeta/gp2notation tests pass.
Closes #527. Part of got-feedback/feedback#334.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
587fbbea81
|
feat(core): consume song_timeline tempos + time_signatures + per-chart tempos (feedpak 1.2.0) (#529)
feedpak 1.2.0 added song-level `tempos` + `time_signatures` to song_timeline.json
and a per-chart `tempos` override on arrangements (§6.10). Core stored the raw
song_timeline dict but never consumed the maps, and didn't read per-chart tempos.
- song.py: shared `sanitize_tempos([{time,bpm}])` (finite non-bool time, finite
bpm>0, sorted); `Arrangement.tempos` field wired through arrangement_to_wire
(omitted when None/empty per §6.10) / arrangement_from_wire.
- sloppak.py: `_sanitize_time_signatures([{time,ts:[num,den]}])`;
LoadedSloppak.tempos / .time_signatures, loaded from song_timeline.json
INDEPENDENTLY of beats/sections (all are optional in 1.2.0).
- server.py: stream `tempos` + `time_signatures` highway-WS messages; the active
arrangement's per-chart `tempos` overrides the song-level map for that chart.
Renderer/UI surfacing is a thin follow-up; this lands the data plumbing.
Codex-reviewed: clean (no findings). +9 tests (sanitizers, per-chart wire
round-trip + omit-when-absent, song-level load/sanitize/absent + maps-without-
beats). 90 song/sloppak tests pass. (Pre-existing unrelated failure:
test_diagnostics_redact, fails on clean main too.)
Closes #526. Part of got-feedback/feedback#334.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
e64378da78
|
feat(core): consume keys.json — song-level key/scale track (loader + WS) (#528)
The feedpak spec defines keys.json (instrument-independent key/scale-change
track, §7.7) but core never loaded it. Add it, mirroring the song_timeline /
drum_tab side-file pattern:
- lib/sloppak.py: LoadedSloppak gains a `keys` field; a permissive loader reads
the manifest `keys:` key, path-safety-checks it, and stores a SANITIZED
{version, events:[{t, key, scale?}]} — finite non-bool t (bad-t events dropped,
not rewritten to 0), non-empty string key, optional string scale, sorted.
Missing / unreadable / malformed -> None, never fatal. int-only version
(a float/NaN version can't abort the load).
- server.py: stream a `keys` highway-WS message when present + a `has_keys`
song_info flag so a consumer can light up a key/scale display.
Renderer/HUD surfacing is a thin follow-up; this lands the data plumbing so
the highway, plugins, and the upcoming scale-degree (`sd`) annotation can read
the active key/mode from the WS.
Codex-reviewed (2 rounds: version-int-coercion + bad-t-drop hardening); clean.
+7 loader tests (happy path, absent/permissive variants, sanitize/sort,
non-int-version no-abort). 150 sloppak/load tests pass.
Closes #525. Part of got-feedback/feedback#334.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
293dc86d83
|
fix(gp): GP5 chord enrichment — gate on diagram-matches-played + decouple name/fingers (#523)
Post-merge Codex review of E3 (PR #522) flagged two GP5 edge cases: - P2: enrichment applied the diagram's name/fingers to the played template without checking the diagram described the voicing actually played. A mismatched chord label/diagram could mis-name/finger the played template, and the back-fill spread it to other strums of the same played pattern. Now gated on an exact, full-span fret-pattern match (new _chord_diagram_frets), mirroring the GP8 guard — and comparing over max(played width, num_strings) so a 7/8-string diagram can't falsely match a narrower played voicing. - P3: name and fingers back-fill were coupled (a name-only first annotation blocked a later beat's fingers). Now independent. Codex re-reviewed twice (the first match-gate trimmed extended strings; fixed by the full-span compare); final pass clean. +4 tests (mismatch-not-applied, name-then-fingers decoupled, higher-position absolute match, 7-string extended string regression). 163 GP tests pass. Follow-up to #522. Part of got-feedback/feedback#334. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c557742174
|
feat(gp): extract GP chord-diagram fingerings + GP8 chord names (E3) (#522)
GP imports previously landed chord templates with blank fingerings (GP5 +
GP8) and blank names (GP8), so the editor/highway had nothing to show even
though E0 preserves and E1 authors that data. E3 extracts the real chord
diagrams so imports arrive rich, keyed on the same fret-pattern join key the
editor (E0) and GP5 already use.
GP8 (lib/gp2rs_gpx.py): parse the per-track GPIF DiagramCollection
(Item @name + Diagram Fret/Fingering) into a fret-pattern -> {name, fingers}
map and enrich matching played voicings at the template build site. Diagram
string indices share the note String index space, so they go through the same
pitch-rank transform; <Fret fret> is treated as absolute.
GP5 (lib/gp2rs.py): pyguitarpro exposes the voicing on beat.effect.chord
(.strings indexed 0=highest string, .fingerings the parallel Fingering enum,
already RS finger ints). New _chord_fingers maps them to RS string order; the
template enrich now back-fills any still-blank template so the annotated chord
attaches even when an earlier unannotated strum of the same voicing created it.
Finger encoding (none/open=-1, thumb=0, index=1, middle=2, ring=3, pinky=4)
matches the editor E1 + RS serializer. Only enriches on exact fret-pattern
match; diagram-less charts import identically (blank). Verified end-to-end
against real files (GP8_Test.gp, joplin-janis-piece_of_my_heart.gp4).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
21997f4b5c
|
Fix slow library cover loading: serve sloppak art without unpacking + revalidated caching (#534)
* sloppak: read cover without unpacking + serialize/cap zip unpacks
Album art for a zip-form sloppak was served by resolve_source_dir(), which
unpacks the ENTIRE archive (stems included, ~30 MB) to disk just to read
cover.jpg. On the library grid that meant a full extraction per card on scroll.
- read_cover_bytes(): opens only the cover member from the zip (or reads the
file for dir-form), with zip-slip guarding. ~4 ms vs a full unpack.
- resolve_source_dir(): per-file lock + bounded global semaphore so concurrent
callers don't rmtree + re-extract the same dest at once (a race), and a burst
can't saturate disk/CPU. 8 concurrent calls now dedupe to 1 unpack.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* server: serve sloppak art via read_cover_bytes + cache album-art responses
- get_song_art() sloppak branch now reads the cover directly (no full unpack),
off-thread via asyncio.to_thread.
- All art responses carry Cache-Control: public, max-age=86400. URLs are already
cache-busted with ?v=<mtime>, so the browser stops re-fetching every cover on
scroll-back; day bound self-heals any URL missing ?v.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* v3 library: lazy-load + async-decode card cover images
The grid (24 cards/page) and artist-row thumbnails emitted plain <img> with no
loading hint, so a whole page of covers fetched + decoded at once on each
scroll batch. Add loading="lazy" decoding="async" to defer off-screen fetches
and keep image decode off the main thread.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* address Codex review: zip cover normalization + correct art revalidation
Findings from the preflight Codex passes:
- sloppak.read_cover_bytes (zip form) read the raw manifest cover string via
zf.read(), so a non-canonical name like './cover.jpg' or 'art/../cover.jpg'
404'd. Normalize via safe_join → relative member; reject escape and the
degenerate root-collapse case ('.', 'subdir/..') like _unpack_zip does.
- Album-art caching is correctness-first: Cache-Control: no-cache plus a strong
validator, with real conditional handling (Starlette FileResponse emits an
ETag but doesn't evaluate If-None-Match). All three art paths route through
_art_conditional/_file_art_response → bodyless 304 on a matching validator.
A long immutable max-age was rejected because the frontend ?v=<mtime> buster
is only second-resolution and would pin a same-second rewrite.
- The sloppak cover is validated by CONTENT (sha1 of the bytes), not a stat:
a dir-form sloppak edited in place changes the cover file's mtime but not the
directory's, so a dir-stat ETag could emit a stale 304. Content hashing is
correct for both dir- and zip-form. get_song_art gained an optional request
(internal get_art caller passes none — safe).
Adds tests/test_sloppak_cover_art.py pinning read_cover_bytes (canonical,
non-canonical, degenerate/escape, dir/zip, webp) and the endpoint's 304 contract
incl. the dir-form in-place-edit no-stale-304 regression.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
fb06e288e1
|
feat(onboarding): input-device setup step + core-owned midi-input domain (#526)
* feat(capabilities): add core-owned midi-input control-plane domain (#873, #880) The MIDI analog of audio-input: a core-owned provider-coordinator over MIDI device discovery, selection, and shared open/close sessions. Separate from audio-input (whose source/open contract is audio-frame-centric) and not owned by any feature plugin, so the device-access boundary outlives the input-setup wizard. `discover` is the Web-MIDI permission boundary; selection persists by redaction-safe logicalSourceKey; diagnostics redact device labels and never carry raw MIDI messages. - static/capabilities/midi-input.js + load-order wiring in both shells - spec 012 + capability-domains/safety-matrix entries; midi-control narrowed to mappings-only (split) - 9 domain tests against the real runtime Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(input_setup): bundled plugin owning input-calibration + Web-MIDI provider (#872) Bundled core plugin that supplies the Web-MIDI source provider to the core midi-input domain, owns the input-calibration workflow domain (run/status/ inspect), and renders the per-instrument wizard (guitar/bass -> audio-input + note_detect; keys/drums -> midi-input live note/pad test). Idempotent hydration; redaction-safe. .gitignore allowlists the in-tree plugin. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(onboarding): input-device setup step between paths and calibration (#874) After instrument-path selection and before the note-detect calibration challenge, dispatch input-calibration `run` (fire-and-launch) and await the `calibration-done` event. Fail-soft: a non-handled outcome (plugin/runtime absent) advances immediately so onboarding can never be stranded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(midi-input): ship a built-in Web-MIDI provider in the core domain Move the Web-MIDI source provider out of input_setup and into the core midi-input domain so every consumer (piano, drums, input_setup) gets MIDI devices from the domain without depending on any one plugin being loaded. input_setup is now a pure midi-input requester (manifest role updated). Prepares piano/drums full consumption (#876/#877). +1 domain test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(input_setup): Settings panel to re-run input setup (#878) Adds a settings.html with a "Set up input devices" button (window ._inputSetupRelaunch) that re-runs the wizard for the player's selected instrument paths (from /api/progression; falls back to all instruments). Makes the calibration wizard re-launchable outside first-run onboarding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(midi-control): formalize the midi-input/midi-control split (#882) Narrow the reserved midi-control domain to mappings ONLY (CC/pitchbend/note → action routing), consuming the delivered midi-input domain for device access. Adds spec 013 defining the contract + intended consumers (feedback-plugin-midi, drums learn-mode), updates the safety-matrix row, and cross-references it from capability-domains. Per governance, midi-control stays RESERVED (no runtime domain) until a concrete mapping consumer + tests exist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(onboarding): wait for input_setup before the calibration step (#874) The input-setup wizard is a mandatory onboarding step, but plugins load asynchronously — in the desktop app (40+ plugins) the user can reach path selection and click Next before input_setup has registered its input-calibration owner. The dispatch then got a no-owner outcome and onboarding fell through to the calibration challenge, silently skipping the wizard. Now wait (bounded, 8s) for the plugin's public global before dispatching; fall through only if it never appears. Race-verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(onboarding): add Song directory step after name+avatar (#874) New first-run step (now step 2 of 4: name+avatar → song directory → paths → calibration challenge) where the player sets their songs folder, fixing the "folder not configured" error on a fresh install. Saves to settings (dlc_dir) and kicks a library scan; persists to config.json so it survives restart. A native folder picker is offered on desktop (window.slopsmithDesktop .pickDirectory); web users type/paste the path. "Skip for now" leaves it unconfigured (settable later in Settings). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(input_setup): filter MIDI entries out of the guitar audio-input picker (#876) Other plugins export pseudonymized MIDI sources ('midi-input-N') into the audio-input domain; they aren't audio inputs and the cryptic labels confused the guitar/bass device dropdown. Filter them out so only real audio inputs show. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(input_setup): de-dupe audio input picker entries (#876) The desktop audio engine enumerates the same device under multiple driver types, so the guitar audio-input dropdown showed repeated entries. De-dupe by display label (paired with the desktop fix that surfaces real device names). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi-input): drop vanished devices on re-discovery; reset setup confirm on switch Codex preflight findings: - midi-input domain `_discover()` only upserted enumerated sources, so an unplugged device (statechange re-discovery) lingered in list-sources and later open/select hit stale state. Reconcile each provider's sources against the fresh enumeration (close any live session, keep the selectedKey preference). - input_setup MIDI panel left "Continue" enabled (and the instrument marked done) after switching the device selection following a prior hit. Reset the waiting state + disable Continue on every selection change, and discard a stale open if the selection changed mid-await. +1 reconciliation test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi-input): coalesce concurrent opens; commit shown audio source pre-calibration Codex re-review (round 2): - midi-input domain: two concurrent open-source calls for the same source both passed the `sessions.get` guard and each called provider.open(), which for the built-in Web-MIDI provider overwrites the shared input.onmidimessage handler and orphans the earlier session — leaving the device silent. Coalesce in-flight opens onto one provider session (await the pending open, adopt its session; re-check after open and release a redundant handle if another open won). +test. - input_setup: the guitar/bass audio <select> shows its first option by default but fires no `change`, so on a first run with nothing selected, audio-input was never told before launchCalibration(). Commit the shown option on render. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi-input): longer timeout for MIDI permission commands; stale-open guard in wizard Codex re-review (round 3): - The advertised command surface ran `discover`/`open-source` through the 250 ms default handler timeout, but those front a real Web-MIDI permission prompt / device open that commonly takes longer, so dispatch returned `failed` while the operation was still completing. Add per-(capability,command) timeout overrides (15 s for those two), folding the existing audio-mix special-case into the same table so both the command() and dispatch() paths honor it. - input_setup MIDI panel: openSelected() compared the mutable shared `activeKey` after its awaits, so a device switch mid-open could bind the old device's listener / close the wrong session. Capture the requested key in a local and use a generation guard to discard a superseded open. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(onboarding): detect 200-with-error song-dir saves; close MIDI session on skip Codex re-review (round 4): - /api/settings reports an invalid folder as a 200 response with an `error` body (a bare dict return, not a non-2xx status), so saveSongDir's res.ok-only check treated the failure as success and advanced onboarding without saving. Parse the body and throw on `error` too. - input_setup: the opened MIDI test session was only closed on the Continue button, so using the generic "Skip for now" after scanning leaked the listener and kept the Web-MIDI input live. Run teardown on every panel exit via a per-panel cleanup hook invoked by advance(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(input_setup): don't hard-code Web MIDI in the device wizard Codex re-review (round 5): the MIDI panel gated availability on navigator.requestMIDIAccess and filtered sources to providerId === 'web-midi', which defeats the midi-input domain's provider-coordinator abstraction — a native/desktop MIDI adapter registered with the domain would be reported unavailable and hidden from the picker. Gate availability on the domain (window.slopsmith.midiInput) and show every source it surfaces. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
edf8f46866 |
Repoint dead slopsmith URLs -> got-feedback
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c1870a0597 |
Improve wording in terminology cleanup
Replace the placeholder noun left by the previous pass with context-fit phrasing (arrangement XML, chart, custom songs, etc.). |
||
|
|
4148b0e72e |
Purge external-format terminology from code, tests and docs
Reword comments/docstrings/strings and rename identifiers that referenced the external game and its file formats: - format-id "psarc" -> "archive"; local vars psarc_path -> song_path, psarc_base -> tone_base - lyrics provenance value "sng" -> "notechart" (legacy "sng" still accepted) - highway_3d fret-ghost scope value "rocksmith" -> "chords" (invalid/legacy values fall back to the default, preserving behaviour) - neutralise references in prose, test names/data, .gitattributes and docs No functional change beyond the renamed identifiers; all Python compiles. |