Commit Graph
60 Commits
Author SHA1 Message Date
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>
2026-06-27 21:07:44 +02:00
a57d0e3f85 fix(v3): replace broken window.prompt() in Playlists with in-app uiPrompt modal (#614)
window.prompt() is a silent no-op in the Electron desktop shell, so the
Playlists "New Playlist" and "Rename" buttons and the library's bulk
"add selected songs to a playlist" action did nothing. Route all three
through the existing window.uiPrompt() modal (resolves to the string, or
null on cancel; the handlers were already async). window.confirm() works
in Electron and is left as-is.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 20:35:30 +02:00
4480ac2732 feat(v3): promote Audio Engine to a first-class sidebar entry (after Settings) (#613)
The desktop Audio Engine plugin (input device selection, VST hosting, pitch
detection, and the new config Reset/repair UI) was reachable only via the
generic Plugins gallery — per-plugin manifest nav entries aren't surfaced in
the v3 sidebar unless the plugin is promoted. Add it to PROMOTED_PLUGINS
anchored after Settings, plus the matching NAV registry entry so the slot
resolves its label/screen. Desktop-only by construction: the slot is filled
only when /api/plugins reports audio_engine installed, so the web app shows
no dead entry.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 18:16:36 +02:00
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>
2026-06-27 00:47:43 +02:00
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>
2026-06-26 18:58:35 +02:00
13bbfc0b3d refactor(highway_3d): move Butterchurn controls into settings.html (#600)
Addresses the altitude finding from the Butterchurn review: the visualizer's
on/off + slider options shipped as a parallel UI (a ~140-line floating
in-canvas control panel) separate from the plugin's standard settings panel.

Move the standard controls (Background on, opacity, dim-behind-lane + strength,
chart accents + strength, color tint + strength, guitar gain, song gain) into
settings.html, using the plugin's normal settings UI. They persist into the
same 'viz3d_settings' blob the controller already reads; a new module-scope
window.h3dBcApplySettings() hook lets settings.html push changes to a mounted
highway live (it invalidates the controller's settings cache and re-applies).

The in-canvas panel is now ONLY the live preset browser (pick / favorite /
ban / cycle / hold / meters) — things that are inherently live tools and don't
belong in a static settings form. cyclePool/hold and the favorites/bans lists
stay there; reads were made cache-safe (read fresh via _bcLoadSettings) so a
settings.html write can't be clobbered by a stale captured reference.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:09:16 +02:00
6fa5aabba8 fix(highway_3d): re-home Butterchurn panel to a surviving highway (splitscreen) (#599)
The Butterchurn control panel is a singleton, created only when a controller
is created and parented to that controller's wrap. In splitscreen the panel
followed the last-created controller; when that controller was torn down,
destroy() only removed the panel DOM if it was the LAST controller, so with
another highway still alive the panel stayed orphaned on the destroyed wrap
and the surviving highway was left with no visualizer controls.

Track each controller's wrap (ctrl.wrap) and, on destroy with another
controller still alive, re-home the panel+pane onto the surviving primary's
wrap via _bcEnsurePanel (which moves them when connected, or rebuilds them on
the survivor if the old wrap was already detached).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:08:13 +02:00
3bb55ab854 feat(highway_3d): Butterchurn visualizer background style (#598)
* feat(highway_3d): add Butterchurn visualizer background style

Adds an opt-in "Butterchurn (visualizer)" option to the 3D Highway plugin's
Background-style dropdown. When selected, the highway renders over a WebGL
MilkDrop (Butterchurn) canvas that reacts to your playing (guitar input on
desktop, the song <audio> spectrum in the browser) and the chart (beat/note/
chord accents + instrument-color tint). The default stays 'particles', so
existing users see no change until they pick it.

Integrates the standalone "3D Highway + Butterchurn" mod into the bundled
renderer as the 'butterchurn' bg-style (not a fork):
- a self-contained _bc* controller that lazy-loads the vendored butterchurn
  libs only when the style is selected; mount/unmount is driven idempotently
  by the existing bg-style lifecycle (_bcSyncMode in _bgMountStyle) plus an
  explicit teardown in destroy()
- the renderer uses alpha:true with the transparent clear gated on the mode,
  so every other bg style stays byte-identical (opaque clear)
- the fog-scenery <audio> tap is disabled while active to avoid a double
  createMediaElementSource on #audio
- the mod's slopsmith* globals are adapted to the current feedBack* names and
  the vendored asset URLs repointed to /api/plugins/highway_3d/assets/

Vendors butterchurn.min.js + butterchurnPresets.min.js (MIT) + viz-worklet.js
under assets/vendor/; see plugins/highway_3d/NOTICE for attribution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Q9BpGYqUaga9ZJyS3dDPq

* fix(highway_3d): anchor Butterchurn panel to the highway, centered

Addresses three issues found testing the visualizer control panel:
- Attach the panel + preset pane to the 3D highway's `wrap` (position:absolute,
  pointer-events:auto) instead of position:fixed on document.body, so they sit
  on the highway's right edge and only exist while the highway is on-screen
  (no longer linger on the main menu / float at the app edge).
- Re-home the singleton panel to the active highway wrap on mount, so it follows
  whichever highway is showing (e.g. moves off Virtuoso's embedded highway onto a
  normal song's highway) instead of sticking to the first one created.
- Center it vertically (top:50% + translateY(-50%), folded into the slide
  transform) so a top overlay element no longer covers it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Q9BpGYqUaga9ZJyS3dDPq

* fix(highway_3d): harden Butterchurn audio + lifecycle (review #597)

Browser audio reactivity now REUSES the highway's existing shared analyser
(the fog scenery's #audio / stems side-chain tap) instead of opening a
second createMediaElementSource on #audio. The old _bcBrowserSource path:
  - threw InvalidStateError when the fog tap already owned #audio (default
    config), leaving the visualizer non-reactive in the browser, and could
    permanently disable fog reactivity if it tapped first (one-shot/element);
  - rerouted the song through a fresh, possibly-suspended AudioContext, which
    could MUTE playback when butterchurn was selected mid-song;
  - ignored the stems analyser, so it saw only silence on sloppak songs.
_bcCreateController now takes an audioProvider (wired to _bgGetAnalyser) and
connectAudio()s the shared AnalyserNode (a passthrough, so the fog's own
reads are undisturbed).

Also:
- destroy() now closes the AudioContext when we own it (desktop / browser
  fallback), fixing a per-mount leak that hit the browser ~6-context cap
  after a few style toggles. The shared (fog-owned) context is never closed.
- _bgApplyVenueSceneFog keeps the clear transparent while butterchurn is
  active, so the venue scene no longer occludes the visualizer.
- _bcLoadLib no longer caches a rejected promise, so a transient vendor-load
  failure can be retried instead of disabling the feature for the session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(highway_3d): Butterchurn lifecycle + audio re-bind (Codex preflight)

Local Codex preflight on the Butterchurn feature flagged four issues; all fixed:

- WebGL context leak on teardown: destroy() (and the async-init failure path)
  now call _bcReleaseCanvasGL() to force WEBGL_lose_context before dropping the
  canvas, so repeated mount/toggle cycles can't exhaust the browser's WebGL
  context cap.
- Stale shared analyser across songs: the browser path captured the analyser
  once at mount, so a sloppak stems swap (new analyser, often new context) left
  the visualizer reacting to a dead node. update() now compares the live
  _bgGetAnalyser() against what the controller actually bound (boundAnalyser(),
  guarded by ready()) and either reconnects (same context) or rebuilds the
  controller (context changed) via the proven destroy()+_bcSyncMode paths.
- Half-mounted controller on createVisualizer failure: the async .catch now
  cleans up (closes an owned AudioContext, removes layers, marks dead) and
  _bcSyncMode retries when bcCtrl.dead(), instead of leaking and never recovering.
- _bcFfIdx off-by-one dropped accents landing exactly on a seek/loop target
  time; it now uses strict < so the update walkers fire the boundary event.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 14:07:29 +02:00
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>
2026-06-24 17:01:48 +02:00
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>
2026-06-24 17:00:30 +02:00
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>
2026-06-24 16:57:37 +02:00
873ee3d5f2 fix(onboarding): rename diagnostic sloppak to feedBack- so "Play it now" finds it (#586)
The slopsmith→feedBack rename updated the diagnostic constant in server.py
(_BUILTIN_DIAGNOSTIC_SOURCES), the build script, README, and the calibration
test to `feedBack-diagnostic-basic-guitar.sloppak`, but the committed data
file was never regenerated/renamed — it stayed `slopsmith-diagnostic-...`.

Result: _seed_builtin_diagnostic_sloppaks() finds no matching source, silently
skips seeding, and the onboarding "Play it now" button (profile.js step 4 →
window.playSong) loads a file that isn't in the library. The server replies
{"error":"File not found"} and highway.js surfaces it as a native
`Error: File not found` popup. Affects all platforms.

Pure file rename to match the (already-renamed) code; no logic change.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 23:38:22 +02:00
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>
2026-06-23 18:06:46 +02:00
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>
2026-06-23 18:05:45 +02:00
db4a30085b feat(v3 library): clickable arrangement badges in tree view (#582)
The v3 library tree rows showed no arrangement badges, unlike the grid/card
view. Render the same clickable chips in tree rows so both views match, and
clicking a specific arrangement opens THAT arrangement in the highway.

Extract the grid's chip markup into a shared arrChipsHtml(song) (one
<button data-arr="<index>"> per arrangement, capped at 4) and use it in both
songCard and the tree row. No new wiring needed: wireCards() already binds
[data-arr] → playCard(song, index) → playSong(filename, index) for any
[data-fn] scope, and the arrangement index is preserved through
/api/library/artists. Chips are hidden on the narrowest viewports
(hidden sm:flex) so they don't crowd the dense single-line tree row.

Closes #581

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 18:05:41 +02:00
b123ab3258 fix(minigames): drain legacy slopsmith pending queue + alias SDK (#578)
After the slopsmith→feedBack rename (#537) the minigames SDK publishes
window.feedBackMinigames and only drains window.__feedBackMinigamesPending.
Minigame plugins that still use the pre-rename shim register against
window.slopsmithMinigames and queue to window.__slopsmithMinigamesPending
when the SDK isn't up yet, so their specs are stranded in the legacy queue
and never register. In v3, FeedBarcade renders those games as non-launchable
"Loading…" tiles that do nothing on click (the tile itself comes from the
server registry, so it appears even though the JS spec never registered).

Publish window.slopsmithMinigames as an alias and drain the legacy pending
queue too (register() is keyed on spec.id, so double-queued specs register
once). Also fire the legacy slopsmith-minigames-ready event. Bump the plugin
version so the desktop renderer cache-busts the updated screen.js.

This rescues every not-yet-migrated minigame plugin, including community
ones we don't control.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:30:00 +02:00
a43e7b13be fix(onboarding): calibration Tuner step no longer exposes the input-select overlay (#577)
Tester: "at the tune step, pressing the Tuner button starts a second wizard at
the input-select step."

Root cause is stacked full-screen overlays. During onboarding the input-setup
flow runs as #input-setup-overlay (z-210) on top of the onboarding modal
#v3-onboarding (z-200), and note_detect's Calibration Wizard (z-300) launches on
top of that. When the player opens the Tuner, that wizard minimizes itself to
transparent + pointer-events:none so the Tuner (z-1000) is usable — but the
input-setup overlay underneath, still showing its "select your input" card, then
shows through behind the floating tuner and reads as a second wizard.

Two targeted hides so only the active surface is visible:
- input_setup: hide #input-setup-overlay while launchCalibration runs; restore on
  its onDone/onCancel (one always fires on close), so the calibration wizard /
  tuner own the screen.
- onboarding runInputSetup: hide #v3-onboarding for the whole input-setup phase
  (its own overlay replaces it visually); restore in finally before advancing to
  the calibration-challenge step.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:51:59 +02:00
7fb568c727 docs: correct plugin URL casing after the feedBack rename (#576)
Cosmetic follow-up to #537 (doc-only).

- Virtuoso: README + CHANGELOG used lowercase
  `got-feedback/feedback-plugin-virtuoso`; the canonical repo (like every
  other got-feedback repo) is capital-B `feedBack-plugin-virtuoso`. Brought
  it in line with the sibling rows.
- Community plugin references in CLAUDE.md, TODO.md, docs/, and the bundled
  tuner README were over-renamed to `feedBack-*` by the rename, but those
  repos are owned by community members who never renamed them
  (topkoa/stems+notedetect, OmikronApex/tuner, masc0t/update-manager).
  Restored their real `slopsmith-*` names. got-feedback's own `feedBack-*`
  references in the same files are left untouched.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:20:23 +02:00
a8ad02739a fix(v3): refresh library accuracy badge right after a song is scored (#574)
The v3 library loaded the best-accuracy map (/api/stats/best) once into
state.accuracy at render time and only refreshed it on a full re-render.
The play->return flow takes the screen-entry fast-path that restores the
cached grid DOM without re-fetching, so a just-earned score stayed
invisible on the card until the next app restart re-ran render().

stats-recorder now emits a `stats:recorded` event (filename/arrangement)
once the scored POST /api/stats resolves on the server -- the correct
moment, since song:stop fires before the POST completes. songs.js
listens: if the library is the active screen it re-fetches
/api/stats/best and patches the affected card/row badge in place;
otherwise it marks the filename dirty and onV3SongsScreenEnter applies
it on return. A failed fetch keeps the entry dirty so a later trigger
retries instead of silently dropping the update.

Badge markup is factored into a shared accuracyBadge(filename, variant)
(grid pill + tree-row percentage, both tagged .fb-acc-badge) so the
in-place repaintAccuracy can find and replace them without a full list
re-render, preserving scroll and pagination. The old empty song:stop
"refresh lazily next render" placeholder is replaced.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:55:57 +02:00
a07edd9971 fix(v3): make equipped theme recolor the sidebar background (#570)
Equipping a cosmetic theme recolored text, fb-* utility surfaces, and
body, but the left sidebar's navy radial wash stayed on its default — so
the interface read as "only the fonts change, not the backgrounds".

Cause: #v3-sidebar is painted with a hardcoded radial-gradient in v3.css
and carries no fb-* utility class, so theme-core's per-utility override
loop never reaches it (#1e293b == default card, #0f172a == default bg).

Extend cssFor() — which already special-cases body — to re-point the
sidebar gradient at the theme, gated by html[data-fb-theme] so the
default (no-theme) look is untouched. Only background-image is overridden,
preserving v3.css's background-attachment:fixed.

Verified in Chromium against the real tailwind.min.css + v3.css +
theme-core.js: default = navy gradient (unthemed), apply() recolors the
sidebar to the theme's card->bg stops, apply(null) reverts to navy.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:25:39 +02:00
5d0229fc82 fix(onboarding): midi-input multi-provider discovery + home-tour lifecycle (#568)
Addresses Codex review of #526/#528:
- midi-input discover(): one provider's enumerate() rejection no longer aborts
  the whole discovery — other providers (e.g. a native/desktop MIDI provider)
  are still queried; denial is only reported when NO provider enumerates.
- Home tour now waits for a 'v3:dashboard-rendered' event (dashboard.js emits
  it after the #v3-home innerHTML swap) before attaching Shepherd, instead of a
  single animation frame that could latch onto pre-render nodes the async
  dashboard render then replaces.
- "Play it now" onboarding now arms the tour (armPendingFirstRun) to run the
  first time the user returns to v3-home, instead of silently never showing it.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:06:28 +02:00
820a18648a fix(player): stop song name/timer overlapping the section map bar (#567)
The section_map plugin injects #section-map as #player's first child — a
~20px bar pinned to top:0 (z-index:5). #player-hud is also top:0/absolute
but at z-index:10 with only py-3 (12px) top padding, so its song name
(top-left) and timer (top-right) paint on top of that bar.

Push the HUD's content below the bar when it is present. The general-
sibling combinator only matches when #section-map precedes #player-hud —
exactly how the plugin inserts it — so the bar-less layout is untouched.
ID-on-ID specificity overrides Tailwind's .py-3 top padding.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:04:06 +02:00
Byron GamatosandGitHub 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.
2026-06-22 13:27:21 +02:00
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>
2026-06-22 12:29:54 +02:00
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>
2026-06-22 12:08:22 +02:00
530995dd02 fix(v3): scope song search to the library + keep it above the toolbar on scroll (#560)
The topbar search (#v3-search) rendered on every screen and, on the library
screen, was hidden behind the filter toolbar while scrolling (both were
sticky top-0 z-20 in the #v3-main scroller).

- shell.js: wrap the search in #v3-search-wrap (hidden by default) and toggle
  it in syncActive() so it only shows on #v3-songs; bump the topbar to z-30 so
  it always sits above the toolbar.
- songs.js: drop the toolbar's top-0 and pin it beneath the topbar by measuring
  the topbar height (positionToolbar). A ResizeObserver on #v3-topbar keeps the
  offset correct as the topbar height changes (viewport width, search show/hide)
  and fixes the initial position regardless of render()/syncActive() ordering.

Fixes #559

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:26:52 +02:00
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>
2026-06-22 11:04:03 +02:00
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>
2026-06-22 10:07:36 +02:00
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>
2026-06-21 21:18:28 +02:00
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>
2026-06-21 20:55:02 +02:00
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>
2026-06-21 12:04:08 +02:00
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>
2026-06-21 11:58:00 +02:00
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>
2026-06-21 11:57:21 +02:00
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>
2026-06-21 11:03:15 +02:00
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>
2026-06-21 11:02:52 +02:00
f182bd0ab7 fix(highway): make fret-hand finger (fg) hints hideable (default on) (#539)
Post-merge review of #538 noted the fg finger numeral rendered unconditionally on
both highways and couldn't be turned off — only sd/ch sat behind the (default-off)
teaching-marks toggle. A user who finds per-note numerals busy had no way to
declutter.

Add a SEPARATE finger-hints gate that keeps fg shown by default but makes it
hideable, independent of the sd/ch opt-in (so the two defaults — fg on, sd/ch off —
coexist; a single boolean can't express that):

- 2D static/highway.js: _showFingerHints (localStorage 'showFingerHints' !==
  'false', i.e. default on), a fingerHintsVisible bundle flag, and
  get/toggle/setFingerHintsVisible API; gates the fg label.
- 3D plugins/highway_3d/screen.js: mirrors via bundle.fingerHintsVisible !== false
  (default on); gates the fg sprite. sd/ch unchanged.

Default-on preserved (absent localStorage / absent bundle flag => shown); only an
explicit false hides fg. Codex-reviewed: clean. Render test 7/7.

Part of got-feedback/feedback#334.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 08:14:24 +02:00
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>
2026-06-21 07:58:04 +02:00
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>
2026-06-21 07:57:50 +02:00
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>
2026-06-21 00:46:36 +02:00
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>
2026-06-20 23:34:50 +02:00
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>
2026-06-20 23:17:39 +02:00
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>
2026-06-20 21:59:04 +02:00
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>
2026-06-20 21:35:07 +02:00
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>
2026-06-20 21:23:48 +02:00
e8db65afcb feat(highway-2d): render unpitched slides (slu) (#524)
The 2D highway drew pitched slides (sl) but ignored unpitched slides (slu) —
drawNote read only opts.sl. The 3D highway already renders both (slideTrailEnd).

drawNote now draws slu as a dashed diagonal with no arrowhead (no definite
target pitch), keeping the solid arrow+arrowhead for pitched sl. The two are
mutually exclusive in the data. Chord notes flow through the same drawNote, so
chord-note unpitched slides are covered too.

Also fixes a latent pre-existing bug flagged in review: `opts?.sl || -1`
discarded a pitched slide-to-open (sl: 0); now `?? -1` preserves fret-0 targets
and keeps pitched precedence.

Codex-reviewed: no P1/P2; dash state reset on all paths, no pitched-slide
regression. node --check clean.

Closes #336. Part of got-feedback/feedback#334.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:09:27 +02:00
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>
2026-06-20 21:04:12 +02:00
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>
2026-06-20 20:44:45 +02:00
1183f100ee fix(v3): rename the "Shop" nav entry to "Unlockables" (#333)
Rename the user-visible label of the HOME-group nav entry (and the matching
"Open Shop →" button on the progress page) from "Shop" to "Unlockables".
Internal identifiers (nav key 'shop', screen id v3-shop, window.v3Shop) are
left unchanged so wiring/state are unaffected.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 14:57:05 +02:00
Byron GamatosandGitHub 9b71d8ddcb Merge pull request #331 from got-feedback/fix/highway-scoreboard-pref
fix(highway): user-selectable scoreboard (core/detailed/off)
2026-06-20 14:20:47 +02:00
Byron GamatosandGitHub d5e184d6e0 Merge pull request #535 from got-feedback/nav/promote-rig-slopscale
v3 sidebar: promote Rig Builder + SlopScale to dedicated nav entries
2026-06-20 12:13:55 +02:00
Byron GamatosandClaude Opus 4.8 a72c0d2e17 v3 sidebar: promote Rig Builder and SlopScale to dedicated nav entries
Collapse the per-plugin sidebar list down to the single "Plugins" entry
(the gallery is the one entry point for general plugins) and give two
bundled plugins their own first-class sidebar slots instead:

- SlopScale (manifest label "SlopScale - Practice"), directly under FeedBarcade
- Rig Builder, directly after the Library group

Both are driven by a PROMOTED_PLUGINS table: each slot is anchored after
a nav key and filled by renderPromotedNav() only when the plugin is
present in /api/plugins, so an absent bundle shows nothing rather than a
dead entry that bounces to the Plugins screen. The visible label uses the
plugin's own manifest nav.label (escaped), falling back to the static NAV
label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 11:50:13 +02:00
Byron GamatosandGitHub 8f800e07b1 Update VERSION 2026-06-20 11:29:12 +02:00
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>
2026-06-20 00:22:43 +02:00
Byron GamatosandClaude Opus 4.8 36aeea67ac fix(highway): user-selectable scoreboard to stop duplicate HUDs
The highway showed two overlapping note-detection scoreboards at once: the
core v3 live-performance HUD (#v3-live-performance-hud) and the note_detect
plugin's own HUD (.nd-hud). Both auto-render off the same note:hit/note:miss
events and neither suppressed the other.

Add a Settings → Visualization "Scoreboard" selector (Streak / Detailed /
Off, default Streak) backed by a single source of truth on
<html data-scoreboard>. CSS shows exactly one:
  core (default) → core HUD; hide .nd-hud
  detailed       → .nd-hud; hide the core HUD
  off            → hide both

CSS-based suppression keys the default off ":not(detailed):not(off)", so the
correct HUD is right even before the pref script runs (no flash) and it
robustly hides any .nd-hud regardless of how many note_detect instances load.
Detection itself is untouched — only the duplicate scoreboard panel is hidden.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 20:56:38 +02:00
c7fb074111 feat(onboarding): first-run home tour (spotlight coach marks) (#528)
* style(tour): align tour engine + Shepherd bubbles to the v3 fb-* palette

The tour/help engine shipped its own indigo/blue dark palette (#181830 / #4080e0)
that predates the v3 fee[dB]ack tokens, and the spotlight bubbles themselves used
the vendored Shepherd LIGHT default (white card, black text) — both clashed with
the navy/sky v3 UI behind them.

- Recolor the "?" menu button, popover and first-visit toast to the fb-* tokens
  (card #1e293b, primary #0ea5e9, border #334155, text #f8fafc/#94a3b8, gold
  #e8c040 unchanged).
- Add a dark .shepherd-* override block (loads after the vendored shepherd.css,
  which is left pristine for upgrades): dark bubble + arrow, fb-primary Next/Done
  button, slate secondary button, fb text scale, and bump the modal dim to 0.6 to
  match the onboarding overlay.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(tour-engine): let client/core tours register into the consolidated menu

The tour engine only listed server-discovered plugins (those with a tour.json,
populated from /api/plugins) in the "?" menu, and always prompted unseen relevant
tours via the toast + button pulse. Generalize register() so a core/client-owned
tour can participate:
- `name` registers the tour into the menu catalog (_tourPlugins) so it shows in
  the "?" menu even without a server plugin; never clobbers a real plugin entry.
- `autoPrompt:false` opts the tour OUT of the unseen toast + pulse (for tours
  driven programmatically by their owner), while still listing + running on
  demand. _unseenRelevant honours it.
Both options are additive and default to the prior behaviour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(v3): add stable tour anchors to home cards + instrument badge

Give the first-run home tour stable spotlight targets: #v3-hero on the hero
panel and data-tour="continue" on the three continue/pick/browse card variants
(dashboard.js), and #v3-instrument-wrap on the topbar instrument selector
(badges.js, mirroring the existing #v3-tuner-wrap). The other targets (audio
routing, tuner, profile, sidebar nav) already had stable ids.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(onboarding): first-run home tour (spotlight coach marks)

After a genuine onboarding completion, dim the home page and spotlight one card
at a time with an explanatory bubble + Next, reusing the shared tour engine
(Shepherd). 7 stops: Hero/Start Playing → Continue/Pick → Instrument selector →
Tuner → Audio Routing → Profile → Sidebar nav. Auto-runs once; replayable
forever from the "?" tour menu as "Welcome tour".

- New static/v3/onboarding-tour.js: registers the spotlight tour (screens:
  ['v3-home'], name "Welcome tour", autoPrompt:false) and exposes startFirstRun(),
  gated on the engine's seen/dismissed state so it never repeats; loaded after
  tour-engine.js + dashboard.js.
- profile.js finish(): trigger startFirstRun() only on a real onboarding
  completion (!editing) — a later profile edit must not relaunch it.

Verified headlessly (native core + Playwright): all 7 anchors resolve, the
spotlight advances one bubble at a time in the v3 dark theme, completion marks
seen, startFirstRun is once-only, and the "?" menu lists "Welcome tour".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(capabilities): clear the handler timeout timer once the race settles

Codex round-6: _withTimeout raced the handler promise against a bare setTimeout
but never cleared it, so a handler that resolves first leaves the timer alive
until it fires. Harmless at 250ms, but the new 15s MIDI permission-command
overrides (discover/open-source) kept the event loop alive ~15s after every
successful call (and the test process hung that long) and could accumulate
delayed callbacks across repeated scans. Capture the timer and clearTimeout it in
a .finally on the race. (Domain/capabilities tests now finish in ~0.1s, not 15s.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(midi-input): give the built-in Web-MIDI provider a distinct participant id

Codex round-7: the built-in Web-MIDI provider registered with participantId
'core.midi-input' — the same id as the domain owner. unregisterProvider()
unregisters the provider's participant, so a provider swap/hot-reload would tear
down the domain OWNER too, leaving midi-input with no owner for later commands.
Register the provider as 'core.midi-input.web-midi'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(onboarding): don't start the home tour when launching the diagnostic

Codex round-7: on the final onboarding step, "Play it now" calls finish() (which
started the home tour) and THEN playSong(target). startFirstRun() navigated to
v3-home and scheduled the tour, then playSong switched to the player — so the
tour spotlighted hidden home elements / stole focus from the diagnostic. Gate the
tour on a launchingSong flag (passed by the "Play it now" path); the Skip path
stays on home, so the tour still runs there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 16:32:33 +02:00
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>
2026-06-19 16:31:16 +02:00
byrongamatosandClaude Opus 4.8 cad78857fb Remove the v3 Support Us! donate button (no funding surface)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:52:44 +02:00
byrongamatosandClaude Opus 4.8 edf8f46866 Repoint dead slopsmith URLs -> got-feedback
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:02:04 +02:00
byrongamatosandClaude Opus 4.8 97a1e5fbf5 Fix two regressions in the terminology purge
- diagnostics_redact: keep '.psarc' in the song-filename scrub regex.
  The purge swapped it for '.archive' (not a real extension), which
  would leak real .psarc filenames still on users' disks into
  diagnostic bundles. This is a redaction allow-list, not brand text.
- sloppak: the legacy lyrics-source alias was a no-op
  ({"notechart": "notechart"}), so old manifests with
  lyrics_source=="sng" fell back to "xml" instead of migrating.
  Map {"sng": "notechart"} so the rename stays back-compatible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:19:16 +02:00
byrongamatos 6c110398b4 Clean release snapshot 2026-06-16 18:47:13 +02:00