A3: openInputSource() surfaces the provider's bound device on the command return
(payload.bound) for the trusted caller, kept out of the redacted event/diagnostics.
B: input-setup guitar/bass step gains a Test-input live meter gating Continue on
heard signal, reading note_detect getInputLevel() (same stream the scorer binds).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>
* 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>
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>
* 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>
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>
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>
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>
* 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>
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.
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>
* 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>
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(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>
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>
* 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>
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>
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>
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>
* 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>
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.