mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-19 06:52:38 +00:00
0a8c8945eafed707fe2631028e50693e40d82c49
66
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
15fabb62aa |
Fix v3 Songs A–Z rail: reliable taps, precise drag, hittable size (#653)
* Fix v3 Songs A–Z rail: reliable taps, precise drag, hittable size Follow-up to #634. Three rail bugs reported on macOS + Windows (0.3.0, 2026-06-29 — =Scr4tch=, MajorMokoto): - Taps often did nothing ("clicked O, nothing happened"). pointerdown calls setPointerCapture, after which the browser retargets the follow-up click to the rail container, so the click handler's closest('.v3-azrail-letter') resolved null and a plain tap (no pointermove) had no other path. Drive the jump from pointerdown itself; reduce the click handler to keyboard activation only (e.detail === 0, Enter/Space). - A drag landed short of the release ("where you release isn't where you get sent"). Every letter crossed fired jumpToLetter with behavior:'smooth'; stacked smooth-scroll animations over the virtualized grid lagged and settled imprecisely. jumpToLetter now takes a smooth flag and scrolls instantly ('auto') while scrubbing, animating only discrete taps/keyboard jumps, so the grid tracks the finger and the release lands on the let-go letter. - The rail was too small at 1440p and didn't scale. Letters were a fixed .62rem glued at right:2px (~13px-tall target). They now scale with the viewport (clamp(.72rem, 1.4vh, 1.05rem)), sit off the edge with taller/wider equal-width hit targets and a hover/active highlight so the scrub target is visible. Keyboard arrow-nav and present-letter gating are unchanged. Tests: tests/js/v3_az_rail.test.js gains pointerdown-seek, keyboard-only click guard, and instant-vs-smooth assertions (809 JS tests; the 13 pre-existing unrelated failures are unchanged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(v3): ignore non-primary buttons on A–Z rail pointerdown (PR #653 review) Right- or middle-clicking the A–Z rail (or a secondary multi-touch pointer) no longer triggers a seek; only the primary tap/drag scrubs. 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> |
||
|
|
0d28886d46 |
Fix v3 Songs List View favorite heart staying dim until re-search (#654)
Favoriting from the tree / "List View" flipped the glyph ♡→♥ but the heart stayed grey until a re-search — reported macOS+Windows, open since 0.3.0 / 2026-06-25. One shared wireCards() [data-fav] handler serves both the grid card and the List-View row, but they render with different idle colours (grid text-white, List View text-fb-textDim) and the handler only ever removed the grid's text-white. So in List View text-fb-textDim lingered next to the freshly-added text-fb-accent and won by CSS source order — the glyph changed but the colour didn't, until a re-search re-rendered the row. Each heart now declares its idle colour via a data-fav-idle attribute; the handler swaps exactly that class (so only one colour class is ever present) and writes the new state back onto the in-memory song model so a re-render / virtualized-grid recycle agrees instead of reverting. Tests: tests/js/v3_favorites_toggle.test.js. Full JS suite 810 tests; the 13 pre-existing unrelated failures are unchanged. Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d20b33348b |
feat(v3): host theme read surface — window.feedBack.theme + always-present --fbv-* tokens (#646)
First slice of the host theme contract (#644): give plugins a host-owned way to read the active theme + its device affordances, so a feature renders correctly under any theme instead of binding to whichever one the dev saw. theme-core.js previously only APPLIED themes and emitted --fbv-* vars only while a theme was equipped (nothing to read in the default state), with no read API. Now, all additive + feature-detected: - Always-present default `fb` palette as `--fbv-*` on :root (the un-themed look is unchanged — fb-* utilities still use their compiled defaults; this only hands plugins a stable host token to read + derive surfaces from). Adds two keystone ROLES the palette lacked: `on-accent` (legible fg on the accent fill) and `focus-ring`. - window.feedBack.theme.get() -> {id, isThemed, tokens}; .capabilities() -> {glow, gradients, motion} (the device-affordance signal; recolor-only themes report defaults, a theme may opt out via `capabilities` in its payload, motion is reduced-motion-gated); .prefersReducedMotion(). - Normalized `theme:changed` event from the single apply() chokepoint. The apply side stays on window.v3Theme; the read surface is attached defensively so it survives the feedBack bus being (re)built by capabilities.js regardless of load order. Verified via a headless render (apply/unequip intact, defaults present + restored, capability opt-out honored, event payload correct) + tests/js/v3_theme_read_api.test.js. See docs/host-theme-contract.md. Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
5e78f2f7f7 |
fix(tuner): remove unused settings + fix sidebar panel position (#661)
Removes the Floating Button and Tuning Visibility settings sections and finishes retiring their still-live config: drops the disabledTunings menu filter and showFloatingButton gate from screen.js/ui.js and their persistence in routes.py (retired keys are stripped on write). Repositions the tuner panel opened from the v3 sidebar Plugins popover to anchor beside it via the host's stable plugin-control slot API (falling back to the popover id), clamped to the viewport so it can't open off-screen, and re-anchored on resize. Updates tuner config tests to the retired-key behavior; plugins/tuner 1.3.2 -> 1.3.3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3e2703d8e1 |
feat(keys_highway_3d): audio-reactive ambience + score FX overlay (K4) (#709)
- BG_STYLES port (off/particles/lights/geometric; lights use the pitch-class palette) mounted behind the scene; _bgGetAnalyser/ _bgReadBands (stems-first, one-shot #audio fallback, permanent-failure latch); Ambience intensity + Audio-reactive settings; remounts on style/intensity change - Score-FX overlay canvas (drum_highway_3d pattern): +1 pops at the scored key, ring pulse every 10-combo, milestone bursts at 25/50/100, red wash on 3+ streak break (wrong notes AND swept misses); cleared when idle, removed in teardown - Tests: style id validation + FX defaults (30 total) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d409d55615 |
feat(drum_highway_3d): audio-reactive ambience + score FX overlay (D4) (#708)
- BG_STYLES port (off/particles/lights/geometric) into a renderOrder -1 group; _bgGetAnalyser/_bgReadBands (stems-first, one-shot #audio fallback, permanent-failure latch, 5ms bands cache); Ambience intensity slider + Audio-reactive toggle; remounts on style/intensity/ palette change and across kit-change scene rebuilds - Score-FX overlay canvas (guitar drawScoreFx adapted to internal scoring): +1 pops at the struck lane, ring pulse every 10-combo, milestone bursts at 25/50/100, red wash on 3+ streak break; pooled, cleared when idle, removed in teardown - butterchurn/image/video deliberately out of scope - Tests: style id validation + FX defaults (15 total) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4214ef365e |
feat(drum_highway_3d): materials & themes — studio env map, metal cymbals, BG_THEMES, cinematic, glow/vibrancy (D3) (#712)
- _makeStudioEnv: procedural PMREM environment (no RoomEnvironment addon vendored) -> scene.environment; rebuilt on kit-change, disposed both teardown paths - Cymbals roughness 0.2 / metalness 0.85 / envMapIntensity 1.2 (metal finally reads); discs satin 0.45 + envInt 0.5; floor 0.7 + envInt 0.35; hit bar envInt 1.0 - BG_THEMES port (guitar ids/values; drum 'default' = original palette byte-for-byte; single pick drives clear/fog + board + lane stripes); drumH3dSetTheme + drum_h3d_bg_theme; live _applyTheme - Cinematic lighting toggle (0.3/1.2 on, 0.4/1.0 off = stock) - Glow slider (base * glow*2; 0.5 default = stock) across notes/hit bar/snare stripe; Lane vibrancy slider (stripe base 0.12+0.24v + halo/ghost opacities), stacking under the D2 approach highlight - Fixed pre-existing floor/hit-bar geometry+material leak on kit change - Tests: theme table parity + default preservation + fallbacks (13) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
80fc371f11 |
feat(keys_highway_3d): anti-plastic materials, studio env, themes, gradient sky, cinematic + glow (K3) (#711)
- Note gems -> MeshPhysicalMaterial: clearcoat 1.0/0.18, roughness 0.32, envMapIntensity 0.9 — lacquered glass, not plastic (the explicit ask) - _makeStudioEnv (PORTED drum_highway_3d): PMREM studio -> scene.environment; black keys 0.22 roughness / 1.3 envInt (glossy piano black), whites 0.42/0.55 ivory, floor 0.55/0.15/0.4 stage sheen - Vertical-gradient background (light horizon -> theme clear -> dark deck), sRGB-tagged for the composer path - BG_THEMES port (guitar ids/values; keys 'default' = original palette; themes drive gradient/fog/floor/rails, never the pitch-class colors); keys3dSetTheme + keys3d_bg_theme, live _applyTheme - Cinematic lighting toggle (0.55/1.3 on; stock 0.75/1.1 off); Glow slider across NOTE_EMISSIVE_BASE / consume-flash / key approach-glow - Env RT + gradient texture disposed in teardown - Tests: theme table parity + default preservation (28 total) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
95b0786725 |
feat(keys_highway_3d): hit FX — vibrancy, timing-colored sparks, hit-line kick (K2) (#699)
- Note vibrancy: gem opacity 0.8 -> slider-driven (default 0.92), NOTE_EMISSIVE_BASE 0.08 -> 0.22, lane guides scale with the slider, _applyVibrancy retints the live scene without a chart rebuild - Sparks (PORTED highway_3d, pool 96) at the struck key, colored by _timingHex/_classifyTiming (±100ms window, inner 40% = on-time; delta recovered from judgeHit's noteKey prefix — contract untouched); streak-scaled counts - Hit-line brightness kick on scored presses (exp(-t*6) decay, hitFx slider), folded into the existing pulse incl. the bloom damp gate - Settings: Hit sparks / Timing colours / Streak feedback + Hit feedback intensity + Note vibrancy sliders (keys3d_bg_*, live-applying) - Tests: classifier boundaries, noteKey time round-trip, FX defaults (26 total) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
749af31cc3 |
fix: correctly import and notate multi-staff (piano/keys) tracks from GP8 (#692)
* fix: correctly import and notate multi-staff (piano/keys) tracks from GP8 Fixes bass stave being dropped on import (bar-column enumeration bug) and wrong hand-split heuristic in notation_lift for chords straddling middle C. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: byrongamatos <xasiklas@gmail.com> * fix(gp-import): fold all grand-staff staves, per-stave tuning, playable hand-splits Addresses review on #692 (topkoa): - split_hands: only use the middle-C boundary when both resulting hands are within HAND_SPLIT_SPAN_SEMITONES, else fall back to the largest-gap heuristic — a hard middle-C split otherwise put a 19-semitone (unplayable) span in one hand for bass-under-treble voicings (e.g. E2+B3 under an Em7 shape). - Treat any multi-stave (grand-staff) track as keys end-to-end, so the stave-0 and folded stave-1+ notes share one encoding and note_count (which sums every stave column) matches what actually imports — closing the phantom-count case for grand-staff instruments the name/program heuristics miss (harp, celesta, marimba). - Fold *every* extra stave (stave_columns[1:]), not just stave 1. - Per-staff tuning fall-back to the track-level Tuning property so an untuned staff never yields an empty pitch list (silent note loss); via a shared _parse_tuning helper. - Extract _collect_column_notes / _merge_lh_notes so the GPX LH/RH pair merge and the GP8 grand-staff fold share one implementation and can't drift in tie/timing/dedup handling. - Rebuild filtered_to_raw from the already-computed stave_columns (one source of truth for the counting rule) and drop the dead num_raw_tracks/raw_tracks. Tests: grand-staff fold + bar-column offset (test_gp2notation.py); both middle-C split cases (test_notation_lift.py). CHANGELOG updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: byrongamatos <xasiklas@gmail.com> --------- Signed-off-by: byrongamatos <xasiklas@gmail.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
991eadeff6 |
Merge pull request #694 from got-feedback/perf/highway-frame-hotspots
perf: eliminate per-frame layout thrash, shader-compile spikes, and per-frame allocations in the player |
||
|
|
56419dc789 |
feat(drum_highway_3d): hit FX — sparks, timing colors, lane flashes, kick pulse, open hi-hat (D2) (#697)
- Pooled spark bursts (PORTED highway_3d, pool 160) at the struck lane, timing-colored via _timingHex/_classifyTiming (±50ms window, inner 40% = on-time); streak-scaled counts (streakFx) - Lane flashes resurrected as pooled additive gauss-tex quads at the hit line (timing-colored; red for wrong hits); kick = full-width quad - Kick pulse: camera dip + amber floor wash, exp decay, hitFx-scaled - Approach highlight: lane stripes brighten toward their next note - Open hi-hat: hh_open renders a warm ring around the gem (closes TODO); orthogonal to accent/ghost/flam variants - Settings: Hit sparks / Timing colours / Streak feedback toggles + Hit feedback intensity slider (drum_h3d_bg_*, live-applying) - All FX resources pooled/shared, disposed in BOTH teardown paths - Tests: _classifyTiming boundaries + FX defaults (10 total) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
beefd7bc09 |
feat(keys_highway_3d): foundation for guitar-highway visual parity (K1) (#696)
- setPixelRatio at last: DPR (cap 2; 1.25 when >1 instance) x host adaptive bundle.renderScale — HiDPI displays were rendering at CSS resolution and upscaling (soft/aliased) - Bloom: port _bloomEnsure/_bloomDispose (UnrealBloomPass 0.65/0.5/0.82, MSAA HalfFloat target, ACES<->None switch); hit-line, flames and consume-glow benefit immediately; direct render is the degrade path - First settings panel: settings.html (graphics category) with a live Glow (bloom) toggle; FX scaffold (FX_DEFAULTS/readFxSettings/ window.keys3dSetFx, keys3d_bg_* keys, keys3d:settings event) - Combo/accuracy/best-streak DOM HUD (drum_highway_3d pattern), gated on a live MIDI session - tests/fx_settings.test.js (3 tests; vm harness) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a7ea719652 |
feat(drum_highway_3d): foundation for guitar-highway visual parity (D1) (#695)
- Consume host adaptive-quality bundle.renderScale; fold into DPR like highway_3d (splitscreen proxy: >1 instance caps baseDPR at 1.25) - Bloom: port _bloomEnsure/_bloomDispose (UnrealBloomPass 0.65/0.5/0.82, MSAA HalfFloat target, ACES<->None tone-mapping switch); rebuilt across the kit-change renderer recreation; direct render is the degrade path - FX settings scaffold: FX_DEFAULTS + readFxSettings + drumH3dSetFx (drum_h3d_bg_* keys), Graphics section in settings.html (bloom toggle, default ON, live-applies) - _applyLaneFlashes dead-code comment updated (visual consumer lands in the hit-FX PR) - __test export + tests/data_layer.test.js (vm harness, 8 tests) — picked up by the CI glob from the bundling PR; README refreshed Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
95cb51b2ad |
perf(highway_3d): forceSinglePass on transparent DoubleSide quads
The retrace after the label-swap fix showed getParameters unchanged (~2.5s / ~4% throttled main thread) — the real driver is Three r158+'s transparent-DoubleSide two-pass path: renderBufferDirect renders such objects back side then front side, setting material.needsUpdate BOTH times, i.e. a full getParameters/program-cache lookup twice per object per frame, plus double draw calls. (Found by reading the two-pass branch in the vendored three.module.min.js right next to the getParameters call site.) All 18 transparent DoubleSide materials in this renderer are flat unlit quads — technique markers, sustain rails, chord frames, lane planes, halo bars — where the two-pass self-occlusion ordering buys nothing. Declare forceSinglePass: true on all of them. Also corrects the _setLabelMap comment's churn attribution (that fix removes the label-swap contribution; this one removes the dominant source). Plugin 3.31.1 -> 3.31.2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
59aa70ce5a |
perf: remove throttled-trace residuals — program churn, per-frame rect, HUD clock
A 4x-CPU-throttled retrace (the honest weak-hardware proxy) surfaced three residual per-frame costs; stack attribution pinned each: - getParameters/getProgramCacheKey (~4% of main thread): every pooled label sprite map swap set material.needsUpdate, bumping material.version and forcing full program re-resolution next render. Swapping between two non-null cached textures never changes the compiled program (USE_MAP define unchanged) — new _setLabelMap() helper only flags needsUpdate on a null<->texture transition, used at all 7 swap sites. - getBoundingClientRect (~1.2%): the 3D highway's per-frame canvas-size self-check forced a layout read every frame. The CSS-box drift read now runs every 10th frame (or when the wrap isn't pinned); the backing-store comparison stays per-frame with cheap property reads and forces an immediate box read + applySize when it fires. - set textContent: the core 60 Hz HUD clock rewrote hud-time (and getElementById'd it) every tick for a display that changes 1/s — now write-on-change with a cached element ref. (The remaining textContent writer in the trace is notedetect's badges.js — external repo, to be filed there.) tests/js: resize-reframe shape test updated for the hoisted _bsChanged gate, incl. an assertion that the throttle can never delay the backing-store path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1e2d5a6162 |
Bundle drum_highway_3d + keys_highway_3d as in-tree core plugins (#693)
* Initial commit — clean relaunch * Initial commit — clean relaunch * Remove external game/format terminology from docs and code Reword references to the external game and its proprietary file formats in comments, docstrings, UI text and identifiers; no behaviour change. * Repoint dead slopsmith URLs -> got-feedback Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(midi): consume core midi-input domain + session robustness fixes (#2) * feat(midi): consume the core midi-input domain instead of private requestMIDIAccess (#881) Route Web-MIDI device access through window.slopsmith.midiInput (discover/list/select/open/close) rather than a private navigator.requestMIDIAccess(). One shared device-access boundary with piano/drums/onboarding; retires this plugin's private Web-MIDI. The note-detection 'midi' exact-verdict provider role and the audio-input source export are preserved (now backed by midi-input source data). Saved pick ({id,name}) stays compatible — domain sourceId == the old MIDIInput.id. Live listener is the domain handle's addListener/removeListener; the async init/resume/pause gate and device-vanish handling are kept. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): stop exporting MIDI into the audio-input domain Keys MIDI now lives in the dedicated midi-input domain (#881). Continuing to register pseudonymized 'midi-input-N' sources into audio-input polluted audio device pickers — notably the onboarding guitar audio-input dropdown showed MIDI devices under cryptic names. Unregister any leftovers and register none. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): guard async connect races, denied-discovery latch, key consistency Codex preflight findings on the midi-input consumption: - Don't latch `_midiReady` when discover() resolves denied/unavailable — only on a handled outcome — so reopening retries the permission prompt. - Add a generation guard to async `_midiConnect`: a slower open() from an earlier selection can resolve after a device/None switch and install a stale handle/listener. Discard (and close) superseded opens. - Carry the domain `logicalSourceKey` on the source/selection descriptor and use it for select/open/close instead of synthesizing `web-midi::<id>`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): invalidate pending opens on detach; clear phantom selection; manifest Codex re-review (round 2): - A detach driven by device removal (sources-changed) didn't advance _midiConnectSeq, so a pending _midiConnect open could resume and install a handle/listener for a now-gone source. Bump the generation inside _midiDetach and capture myGen after it, so any later detach supersedes an in-flight open. - If mi.open() yields no handle, _midiInput stayed set — a phantom connected device the render loop's miss-sweeping would penalize. Clear it on the no-handle and catch paths. - plugin.json: replace the stale audio-input provider role (the removed MIDI-into-audio-input export bridge) with the midi-input requester it now uses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): make _midiResume idempotent under multiple live instances Codex re-review (round 3): with the domain addListener API, a second live renderer instance (splitscreen/overlapping lifetimes) calling _midiResume() while already active could register the same listener again and double-deliver a MIDI note to the focused instance (hit + duplicate misses). Return early when already active rather than relying on the provider's Set-backed de-dup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): release domain session on teardown; counter-free legacy source cleanup Codex re-review (round 4): - [P2] Last-instance teardown only detached the listener and never released the midi-input domain session, leaking the requester ref. Add _midiReleaseSession() (delegates to _midiDetach: close + null + generation bump) at the destroy site; re-mount's _midiInit auto-connects from the saved pick. - [P3] The legacy audio-input MIDI-source cleanup looped over the module-local _aiRegisteredCount, which resets to 0 on an in-page upgrade so the prior build's 'keys-midi:input-N' entries were never unregistered. Iterate a fixed bound over the known sourceId pattern instead (unregister of an absent source is a no-op). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): gate miss-sweep on live handle; don't open without a live renderer Codex re-review (round 5): - Miss-sweeping was gated on _midiInput, set as soon as a device is picked, but the async mi.open() may still be pending (slow / permission prompt) — notes passing during that window banked false misses. Gate on _midiHandle, truthy only after a handle is opened and wired. - A settings-only ensure-init (or a discover resolving after the last instance was torn down) could open a midi-input session with no renderer to release it. Gate mi.open() on _instances.size > 0; the pick is saved and a later mount re-runs auto-connect. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): guard open-failure clobber + don't clobber saved device on unplug Bringing keys-highway-3d in line with the piano/drums hardening: - The _midiConnect catch cleared _midiInput unconditionally; a stale older open's rejection could wipe a newer connect's _midiInput/_midiHandle (and leak the handle). Only clear when myGen === _midiConnectSeq. - The sources-changed recovery called _midiAutoConnect(), whose fallback persists a substitute device — overwriting the user's saved pick on a transient multi-device unplug. Parameterize _midiAutoConnect(allowFallback); recovery passes false (reconnect the saved device only, never a persisted fallback). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): keep first-hotplug working; don't tear down a live session on re-init Codex round-11: - The unplug-recovery _midiAutoConnect(false) skipped ALL fallbacks, which broke first-hotplug (open visualizer with no keyboard, plug one in → never picked → no connect). Skip the substitute only when a saved pick exists but is absent (preserve it); allow the fallback when nothing was ever picked. - _midiInit re-ran _midiAutoConnect on every ready re-init (settings/splitscreen), tearing down the live handle + releasing held keys. Only re-connect when there's no live handle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(midi): consume core midi-input domain + session robustness fixes (#2) * feat(midi): consume the core midi-input domain instead of private requestMIDIAccess (#881) Route Web-MIDI device access through window.slopsmith.midiInput (discover/list/select/open/close) rather than a private navigator.requestMIDIAccess(). One shared device-access boundary with piano/drums/keys/onboarding. Saved pick ({id,name}, with legacy id-only fallback) stays compatible — domain sourceId == the old MIDIInput.id. The async in-flight init guard, _midiActive teardown gate, device-vanish handling, and settings device APIs are preserved; the live listener is the domain handle's addListener/removeListener. Degrades to no-MIDI when the domain is absent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): guard async connect races, denied-discovery latch, key consistency Codex preflight findings on the midi-input consumption: - Don't latch `_midiReady` when discover() resolves denied/unavailable — only on a handled outcome — so reopening retries the permission prompt. - Add a generation guard to async `_midiConnect`: a slower open() from an earlier selection can resolve after a device/None switch and install a stale handle/listener. Discard (and close) superseded opens. - Carry the domain `logicalSourceKey` on the source/selection descriptor and use it for select/open/close instead of synthesizing `web-midi::<id>`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): invalidate pending opens on detach; clear phantom selection; manifest Codex re-review (round 2): - A detach driven by device removal (sources-changed) didn't advance _midiConnectSeq, so a pending _midiConnect open could resume and install a handle/listener for a now-gone source. Bump the generation inside _midiDetach and capture myGen after it, so any later detach supersedes an in-flight open. - If mi.open() yields no handle, _midiInput stayed set — a phantom connected device the render loop's miss-sweeping would penalize. Clear it on the no-handle and catch paths. - Declare the midi-input requester capability (degrade-noop) in plugin.json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): make _midiResume idempotent under multiple live instances Proactively mirror the keys-highway-3d round-3 fix (same multi-instance lifecycle and identical code): a second live renderer instance calling _midiResume() while already active could double-register the listener and double-deliver a MIDI hit. Return early when already active. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): release the domain session on final teardown Mirror the keys-highway-3d round-4 fix: last-instance teardown only detached the listener and never released the midi-input domain session, leaking the requester ref. Add _midiReleaseSession() (delegates to _midiDetach: close + null + generation bump) at the destroy site; re-mount's _midiInit auto-connects. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): gate miss-sweep on live handle; don't open without a live renderer Codex re-review (round 5): - [P2] drumH3dEnsureMidiInit (settings-only, no renderer) auto-connected and opened a midi-input session that destroy()/_midiReleaseSession would never run to release — held until reload. Gate mi.open() on _instances.size > 0; the pick is saved and a later renderer mount re-runs auto-connect. - Mirror the keys-highway-3d miss-sweep fix: gate accumulation on _midiHandle (live wired session), not _midiInput (set before the async open resolves), so notes during a pending open don't bank false misses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): guard open-failure clobber + reconnect saved device on replug Bringing drum-highway-3d in line with the piano/drums hardening: - The _midiConnect catch cleared _midiInput unconditionally; a stale older open's rejection could wipe a newer connect's _midiInput/_midiHandle. Only clear when myGen === _midiConnectSeq. - The sources-changed handler only refreshed the list (never reconnected), so a replug of the saved device didn't reattach. Reconnect on sources-changed via _midiAutoConnect(false) — saved device only, no fallback, so a transient unplug can't switch to / persist another input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): keep first-hotplug working; don't tear down a live session on re-init Same two round-11 fixes as keys-highway-3d (shared structure): the recovery _midiAutoConnect(false) now allows a fallback when nothing was ever picked (first-hotplug) while still preserving a saved-but-absent pick; and _midiInit only re-connects when there's no live handle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): select by logicalSourceKey, not provider-local sourceId (#4) Codex review of the midi-input migration: resolving a selection with find(s => s.id === id) treats the provider-local sourceId as globally unique, so when the core midi-input domain aggregates multiple providers two devices sharing a sourceId can't be distinguished — the wrong device opens and unplug detection can miss that the selected key vanished. Thread the globally-unique logicalSourceKey through save/read, _midiConnect, auto-connect reconnect, the public keysH3dSetMidiInput entry, and the capability _aiOpen path, preferring the key and keeping the bare sourceId / name only as a legacy fallback. Mirrors the already-merged drums/piano migration (_midiResolveSaved). Self-reviewed against that reference. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): select by logicalSourceKey, not provider-local sourceId (#3) Codex review of the midi-input migration (same issue as keys-highway-3d): resolving a selection with find(s => s.id === id) treats the provider-local sourceId as globally unique, so when the core midi-input domain aggregates multiple providers two devices sharing a sourceId can't be distinguished — the wrong device opens and unplug detection can miss the vanished key. Thread the globally-unique logicalSourceKey through save/read, _midiConnect, the auto-connect reconnect, and the public drumH3dSetMidiInput entry, preferring the key with the bare sourceId / name only as a legacy fallback. Mirrors the already-merged drums/piano migration. Self-reviewed. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: add plugin metadata (category, description, thumbnail) (#5) * chore: add plugin metadata (category/description/thumbnail) Refs got-feedback/feedBack#571 * chore: add placeholder thumbnail Refs got-feedback/feedBack#571 * chore: add plugin metadata (category, description, thumbnail) (#4) * chore: add plugin metadata (category/description/thumbnail) Refs got-feedback/feedBack#571 * chore: add placeholder thumbnail Refs got-feedback/feedBack#571 * fix(viz): register feedBackViz_<id> so the player viz picker lists this plugin (#5) After the slopsmith->feedBack rename the factory was still registered as window.slopsmithViz_<id>; the host viz picker only looks up window.feedBackViz_<id> (no slopsmithViz alias exists) and silently skipped this plugin. Add the feedBackViz_ global (aliased to the existing slopsmithViz_ one for back-compat). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(viz): register feedBackViz_<id> so the player viz picker lists this plugin (#6) After the slopsmith->feedBack rename the factory was still registered as window.slopsmithViz_<id>; the host viz picker only looks up window.feedBackViz_<id> (no slopsmithViz alias exists) and silently skipped this plugin. Add the feedBackViz_ global (aliased to the existing slopsmithViz_ one for back-compat). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(plugins): bundle drum_highway_3d + keys_highway_3d as in-tree core plugins Import both 3D highway plugins from their standalone repos via git subtree (history preserved), ahead of the visual-parity epic that ports the guitar highway's polish to them. - .gitignore: !plugins/drum_highway_3d/ + !plugins/keys_highway_3d/ exceptions - keys plugin.json: "bundled": true (drum already had it) - CI: JS test step gains 'plugins/*/tests/*.test.js' (+20 keys tests) - static/tailwind.min.css regenerated (core build scans plugins/**) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drum_highway_3d): narrow Auto-mode predicate so bundling can't steal guitar arrangements has_drum_tab is pack-level; first-match-wins Auto order sorts this plugin before highway_3d. Claim only drum arrangements, or packs nothing more specific can render (Codex preflight P2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: CHANGELOG — note the deliberate Auto-predicate narrowing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drum_highway_3d): MIDI lifecycle — no false misses on mid-song connect, promote survivor on destroy - _missSweepFloor exempts notes that passed before the device wired up from the miss sweep (lowered on seek-back; cleared with scoring resets) - destroy() promotes a surviving instance to _activeInstance so splitscreen panel teardown doesn't drop all MIDI routing (Codex preflight round 2, both pre-existing in the imported code) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Sin <deathlysin@outlook.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
77547af110 |
perf: allocation/scan hardening for weaker hardware
Static-analysis follow-ups to the trace-backed fixes; each is cheap insurance on machines where the profiled headroom doesn't exist. - highway.js: _makeBundle now mutates one persistent per-instance object instead of allocating a fresh ~35-field bundle every rAF frame (xN under splitscreen). Object identity is stable and meaningless; array fields still swap reference on chart changes, which field-identity caches rely on. Contract documented in both CLAUDE.mds. - highway.js: new bsearchTime (lower-bound on .time) windows the default 2D renderer's beat-line scan (was O(all beats) per frame); bundle.lowerBoundT / bundle.lowerBoundTime expose the searches to custom viz so they stop reimplementing visible-window culling. - highway_3d: localStorage 'h3d_full_sus' polled at ~1 Hz instead of every frame (synchronous storage read on the hot path). - highway_3d: drawLyrics caches the measureText row layout keyed on (lyrics ref, line index, shown count, font size, width) — per-frame work is now just drawing over cached widths. - tests/js: bundle source-shape assertions widened to accept the assignment form ([:=]) alongside the old object-literal form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
115c96a3f0 |
feat(tuner): gate playback until you've tuned — hold autoplay + no-trap Skip/Back/Esc (working-tuning PR 4) (#666)
* feat(tuner): gate playback until you've tuned — hold autoplay + no-trap Skip/Back/Esc (working-tuning PR 4) When the opt-in auto-open fires because a song needs a different tuning, playback now WAITS behind the tuner instead of starting underneath it — the "tune before you play" model. Built on a new generic core hook window.feedBack.holdAutoplay() (mirrors holdAutoExit): the tuner claims the hold synchronously on song:loading (beating the song:ready autostart) and releases it — or a 12s fail-open backstop does — so a wedged plugin can never strand a song. Generation-guarded; manual Play always wins. No one-way trap: - Skip = "I've tuned" -> plays and records the song's tuning as the instrument's current working tuning (the explicit write-point PR 3 left as 'assumed'). - Back to library / Esc -> leave the song, record nothing (reuses requestExitSong; Esc is the existing player shortcut). - The in-panel x is dropped for an auto-open — Skip/Back/Esc are the dismiss surface. This also keeps the write honest: Skip is the only on-player dismiss that records, so leaving never falsely records a tuning. Stacked on #660 (working-tuning PR 3). Core app.js gains only the generic hook (a test asserts it never references the tuner's internals); shell-agnostic. Needs a desktop smoke-test that the tuner mic doesn't contend with note_detect's scoring input under ASIO/exclusive mode (per the design charrette). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(tuner): backstop can't cut off tuning + gate race/token hardening (PR #666 review) Review fixes for the autoplay gate: - The 12s fail-open backstop could start playback UNDER a legitimately-open tuner (a slow / mic-verify retune > 12s). holdAutoplay()'s release now carries a .settle() that cancels the backstop; the tuner calls it once the tuner is confirmed open (_gateClaimed), so the hold becomes deliberate and only a dismiss / song switch releases it. (Fail-open still covers "claimed but wedged before deciding".) - The async song:ready handler could release a NEWER song's gate after its await (global _gateClaimed, no guard). It now snapshots _autoOpenGeneration and bails if a newer song took over. - holdAutoplay guarded by song generation, not per-hold — a stale release from an earlier hold could clear a later one. Each hold now mints a unique token that release()/settle() must match. Tests: source-level assertions for the token, settle(), the settle-on-open call, and the song:ready gen-guard. 45 tuner+speed tests green. Codex-reviewed. 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> |
||
|
|
6aed8510d7 |
Tuner: passive "different tuning" badge cue naming the retune (issue E, stage 2.5/3) (#657)
* Tuner: passive "different tuning" badge cue that names the retune
Building on the coverage check: when you enter a song your current
instrument doesn't cover, the topbar tuner badge gets an amber ring + a
tooltip naming the change (e.g. "retune B->A", or "the reference pitch"
for an A440 vs A432 mismatch). Advisory only -- it never auto-opens the
panel; recomputed on song:ready, cleared on song-load / leaving the
player.
Refactors the coverage check into a structured report
(window._tunerAutoOpen.coverageReport -> { covered, retune:[{from,to}],
reference, cantCover }); the boolean gate now wraps it. The cue is
CSS-free (inline ring + native tooltip, no Tailwind rebuild) and no-ops
when the tuner plugin is absent.
Touches static/v3/badges.js (cue) + plugins/tuner/screen.js (report).
v3-only. Stacked on #656 (issue E stage 2.5/3). The splitscreen-suppress
and no-usable-input guards move to E2 (the playback gate).
Tests: tests/js/tuner_auto_open.test.js (report names the strings,
reference mismatch, badge wiring).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF
* feat(tuner): read/write the live per-instrument working tuning — both-directions retune prompt (working-tuning PR 3) (#660)
The §4 coverage check compared each song against the player's fixed
instrument-profile tuning, so the tuner only ever prompted *away* from a "home"
tuning (E -> Drop C#) and stayed silent coming back (Drop C# -> E), even though
the player had physically retuned.
_playerTuning() now reads the host's live per-instrument working tuning
(window.feedBack.workingTuning, keyed by the selected instrument from
/api/settings) instead of re-deriving from the static settings tuning, so
coverage is measured against what the instrument is ACTUALLY in and prompts both
directions. On clearing an auto-opened tuner, _publishWorkingTuning() writes that
song's tuning as the instrument's live working tuning ('assumed' — PR 4's
explicit "I tuned / Skip" refines the write-point), so the next song is judged
against where the player now is.
Per-instrument (guitar vs bass tracked separately). Feature-detected: falls back
to the static /api/settings tuning when the working-tuning capability is absent,
so the 27 existing coverage tests are unchanged. Builds on PR 1 (host
workingTuning) + PR 2 (instrument->chart routing).
Tests: tests/js/tuner_auto_open.test.js — +2 (both-directions coverage via a live
Drop-D working tuning; publish-on-clear targets the right instrument slot); 29
pass total.
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(tuner): transactional open + fail-closed auto-open config (tuner-E #655 review) (#681)
Two review fixes for the auto-open opt-in+persist stage:
- enable() wasn't transactional. The panel (with the ×/Skip buttons) is shown
before `await _tunerAudio.start()`, and `_state.enabled` was only set after it.
A ×/Skip dismiss during that await hit disable() with wasEnabled=false, then
enable() completed and flipped enabled on — an enabled-but-hidden zombie. Guard
the open with an `_openGen` token bumped on every enable()/disable(); after the
audio-start await, bail if superseded instead of enabling. Closes #675.
- Config wasn't fail-closed. routes.py normalized the opt-in with
bool(data.get("autoOpenOnTuningChange", False)), so "false"/"0"/junk coerced to
True. Accept only a real JSON boolean. Closes #676.
Tests: tuner_auto_open.test.js (dismiss-mid-open stays disabled — fails without
the token guard), test_config.py (auto-open default-false + fail-closed on
non-bool). 34 JS + 24 config tests green.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(tuner): coverage stays conservative when the instrument is unknown (tuner-E #656 review) (#682)
_playerTuning() is documented as conservative ("missing data → not covered → still
prompt"), but when /api/settings carried no instrument identity (a fresh profile:
_default_settings() omits instrument/string_count/tuning) it invented guitar/6/440/
standard, so an unconfigured player was treated as 6-string E-standard and coverage
suppressed the auto-open (and badge cue) for matching songs. The post-#660 rewrite
only returned null when the whole fetch failed (!s), not when settings existed but
lacked an instrument.
Now return null unless there's a confident identity — any of instrument/string_count/
tuning in settings, or live working-tuning offsets. A configured standard guitar still
covers a standard song (no regression). Closes #677.
Tests: tuner_auto_open.test.js — empty-settings → not covered (fails without the fix);
configured standard guitar → still covered.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(tuner): badge coverage cue staleness + unknown-as-warning + dedupe (tuner-E #657 review) (#683)
Three review fixes for the passive "different tuning" badge cue stage:
- Stale async cue (#678): _refreshCoverageCue awaited coverageReport then wrote the
DOM unconditionally, so a slow /api/settings fetch could restore the previous
song's amber ring after song:loading / leaving the player. Add a monotonic token
bumped on every refresh and both clear paths; apply the awaited report only if the
token still matches.
- "Unknown" rendered as "needs retune" (#679): the plugin returns a conservative
all-false report on a fetch hiccup; the cue painted that as an amber "retune the
reference pitch" ring. Collapse a no-signal report (not covered, no reference /
retune / cantCover) to null (no cue) via _meaningfulReport(). A genuine not-covered
report always carries reference / retune / cantCover, so real cues are preserved.
- Duplicate /api/settings fetch (#680): the auto-open gate and the badge cue both
call coverageReport() per song:ready. Cache the coverage promise per song (keyed by
session + tuning + centOffset) so they share one fetch; invalidate on song:loading,
instrument:changed, and working-tuning-changed so it can't go stale within a song.
Tests: tuner_auto_open.test.js — concurrent reports share one fetch, a new song
refetches (fails without the cache). 34 JS tests green. Codex-reviewed.
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: Byron Gamatos <xasiklas@gmail.com>
|
||
|
|
6b0e37aa35 |
Tuner auto-open: instrument-coverage check (issue E, stage 2/3) (#656)
With the opt-in auto-open on, only prompt when the player's current physical tuning doesn't already cover the song. FeedBack is tune-to-song (the highway draws tab in the song's tuning), so the check aligns the song's open-string tuning string-for-string against the player's instrument: - An 8-string F# player gets no prompt for a 6-/7-string standard song (its top strings already match those tunings). - A song needing an open string the player lacks (e.g. a Drop-A 7-string's low A on an F# 8-string) still prompts. - A whole-instrument reference difference (A440 vs A432, or an octave-down centOffset, previously ignored) also prompts. Reads the player's instrument from core /api/settings (the v3 instrument selector, a stable physical reference); conservative fallback (prompt) when undeclared or unavailable, so a real retune is never silently skipped. v3-only. All in plugins/tuner/screen.js; no core changes. Stacked on #655. Follow-up E1.6: a passive badge cue that names the strings to retune, plus splitscreen / no-usable-input guards. Tests: tests/js/tuner_auto_open.test.js (covered/uncovered, the Drop-A case, reference mismatch, direct contiguous alignment). Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6bfd92aa06 |
Fix tuner auto-open flash: opt-in + persist (issue E, stage 1/3) (#655)
The tuner self-closes on song:play; autoplay fires it right after a song switch, so an auto-opened tuner flashed shut ~1s later. An arrangement switch (which never arms autoplay) instead persisted — the opposite tester reports, and not the mic. - New opt-in setting autoOpenOnTuningChange (tuner Settings, default OFF) - An auto-opened tuner persists: it ignores the autoplay song:play, stray outside-clicks, and same-screen re-emits, closing only via the new in-panel x / Skip buttons or leaving the song. A manual open keeps the classic click-away / play-to-close behaviour. - Adds the panel's first in-box close (x + contextual Skip). - All in the tuner plugin; no core app.js changes. Default (opt-in vs opt-out) is teed up for Byron to flip one boolean. Staged follow-ups: E1.5 = instrument-coverage smart prompting + badge cue; E2 = holdAutoplay gate. Tests: tests/js/tuner_auto_open.test.js (opt-in gate, persist mode, play/click-proofing). Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7713ca92f4 |
Merge remote-tracking branch 'origin/main' into feat/feedpak-jsonc
Signed-off-by: topkoa <topkoa@gmail.com> # Conflicts: # CHANGELOG.md |
||
|
|
c4bb58233d |
feat(core): route the highway chart to the selected instrument's part (working-tuning PR 2) (#659)
When a song loads without an explicit arrangement, highway_ws now reads the player's selected `instrument` from config.json (the same file it already reads for the default-arrangement preference) and picks the arrangement that matches: bass -> the Bass part. Guitar — and any unknown/future instrument (drums, keys) — falls through to the existing preference/most-notes default, which already lands on a guitar part. Previously the instrument selector only fed the tuner, so a bass player was handed the default Lead/guitar chart, and the working-tuning coverage check then compared a 4-string bass against a 6-string part (always "can't cover"). This is the instrument->chart routing the working-tuning series leans on. Server-only (every launch path flows through the WS, so no client change). An explicit arrangement request always wins, so only the default part chosen on load changes. Tests: tests/test_highway_ws_instrument_routing.py (bass->Bass, guitar->default, explicit-wins) — 3 new, existing highway WS tests still green. Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
491039a12d |
feat(core): host per-instrument workingTuning capability + read-API/event (working-tuning PR 1) (#658)
* feat(core): host per-instrument workingTuning capability + read-API/event (working-tuning PR 1)
Introduce window.feedBack.workingTuning — the live, host-authoritative current
instrument tuning (offsets + string-count + reference pitch + assumed/verified
provenance), distinct from any one song's tuning and from a soft opt-in default.
It's the single source of truth the highway, library, and plugins (tuner,
Virtuoso, minigames) will read so a retune or instrument swap is reflected
app-wide instead of being re-derived per surface.
PER-INSTRUMENT: state is a map keyed by `${instrument}-${stringCount}` (e.g.
guitar-6 / bass-4, the selector's key) — your guitar's tuning and your bass's are
kept separately; get() returns the selected instrument's, and switching the
selector surfaces that instrument's own remembered tuning. You only ever deal
with the one you've picked.
Modeled on the shipped `tuning` capability + the `feedBack.theme` read-API:
synchronous get(instrument?), set(state,{provenance,instrument}) mutator,
setCurrentInstrument(), resetToDefault(), and a `working-tuning-changed` event
that fires on change and once on hydration (carrying which instrument changed).
In-memory, seeded from /api/settings, reset-on-restart. Registered as a separate
`working-tuning` exclusive-owner capability (tuner = sole writer, others read).
Foundation only — pure plumbing, nothing writes to it yet and no behavior
changes. The tuner becomes the writer (and the gate's E->C# asymmetry is fixed)
in a later PR.
Frontend-only: new static/capabilities/working-tuning.js, loaded from
static/index.html + static/v3/index.html. Per-instrument state machine verified
by a stubbed node harness (separate guitar/bass slots, selector switch, isolated
writes, verified stamp, reset, defensive copies, capability registration).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF
* fix(working-tuning): resolve review P1/P2s + add behavioral test harness
Addresses the manual + Codex review of PR 1 (working-tuning foundation).
P1 — named tunings were dropped by the boot seed: /api/settings.tuning may be a
name ("Drop D") OR an offsets list, but the seed only handled the list and stored
offsets:null for names. The seed now resolves a name to per-string semitone offsets
via /api/tunings (ratio vs Standard; reference pitch cancels).
P1 — async seed could clobber state a consumer had already written: _seedFromSettings
resolves after boot and used to overwrite _currentKey/_byInstrument unconditionally.
It now bails when state was already _touched (and re-checks after the /api/tunings
leg), so an explicit set()/setCurrentInstrument()/resetToDefault() before hydration
wins. Hydration still fires.
P1/P2 — shallow copy leaked live nested arrays: get() and set() now clone offsets and
verifiedStrings on both ingress and egress, honouring the "readers can't mutate live
state" contract.
P2 — provenance/verification state machine made coherent by construction:
verified <=> verifiedStrings is an array AND verifiedAt is a finite number. A tuning
change invalidates prior verification unless a fresh bundle is supplied; a "verified"
claim with no strings or a null/absent timestamp is repaired (assumed / stamped now).
P2 — bare-instrument writes targeted a hard-coded default string count: _keyOfResolved()
resolves an omitted string count against the current selection (same instrument), so
set({instrument:'bass'}) / set({stringCount:5}) hit the selected bass-5, not bass-4.
Test — adds tests/js/working_tuning.test.js (the harness the PR described but did not
commit): 11 behavioral cases over a stubbed window — registration, per-instrument
isolation + selector switch, defensive copies, the verification invariant, bare-key
routing, named + offsets-list seeding, and the boot-race guard. Full tests/js suite:
no new failures (the 12 pre-existing branch failures are unrelated).
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>
|
||
|
|
3120ae3e71 |
fix(highway_3d): dolly back so the fret-number row can't clip off the bottom (#633)
The heat-coloured fret-number row is drawn as a band BELOW the board (sY(lowest) - S_GAP*1.4), but camUpdate's self-correcting framing only anchors the board CENTRE to the lower third of the screen and reserves no headroom for that row. So a tight zoom on a centred active span (worst mid-neck; fine at either end of the neck) pushes the numbers past the bottom edge -- which is why testers saw it "only when centered" and "not every song." Tilt can't fix it (it would only trade a bottom clip for a top clip); the vertical-extent problem at tight zoom needs camera distance. Add a fret-row fit guard: project the row band with the final camera and, when it falls below FRET_ROW_FIT_NDC_MIN, raise a capped, hysteretic _fretRowFitBoost applied to the curDist lerp target (the span-driven tgtDist still owns zooming IN). The boost rises promptly (proportional to the deficit), relaxes lazily past a deadband, and is capped at FRET_ROW_FIT_BOOST_MAX (+60%) so the zoom can't pop or hunt. It cooperates with the tilt loop (pull-back shrinks the scene, tilt keeps the centre anchored) and yields entirely to the Camera Director free-cam. Surgical: passages where the row is already visible never trigger it, so framing is unchanged everywhere it already worked. plugin.json 3.30.0 -> 3.30.2 (screen.js cache-buster; 3.30.1 is taken by the FPS-counter PR). Tests: tests/js/highway_3d_camera_framing.test.js (guard constants, the boosted curDist lerp, the projected-row hysteresis, free-cam yield). Fixes #632 Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
199550e5fb |
fix(v3): dismiss Section Practice popover when another player popover opens (#638)
* fix(v3): dismiss Section Practice popover when another player popover opens The Section Practice popover (Songs > Song > Practice pill) stayed open when the user then clicked a v3 player-rail icon (Plugins, Audio, …), leaving two popovers stacked on top of each other. Reported on 0.3.0 (macOS) and still reproducing in the 2026-06-28 build. Root cause: the popover's outside-click dismiss was bound in the bubbling phase, but the v3 rail's icon buttons call e.stopPropagation() in their click handler (player-chrome.js wireRail), which kills bubbling before the click reaches document. So the dismiss listener never fired for a rail-icon click and the popover was orphaned open. Fix: bind the outside-click dismiss in the capture phase, which runs before the target's handler so stopPropagation() can't swallow it. This mirrors the audio mixer popover (audio-mixer.js), which already dismisses outside-clicks via capture-phase listeners for exactly this reason. Esc handling stays in the bubble phase (no rail handler stops keydown propagation, and capturing it would reorder it ahead of the player's Escape-to-exit handling). Shared app.js code, so v2 is covered too; v2 has no stopPropagation rail, so its outside-click dismiss behaviour is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * chore(#638): add CHANGELOG entry + capture-phase regression test Review follow-ups for the Section Practice popover dismiss fix: - CHANGELOG [Unreleased] → Fixed entry (repo workflow requires one). - tests/js/section_practice_dismiss.test.js pins the fix: the outside-click dismiss binds in the CAPTURE phase (so a rail icon's stopPropagation can't swallow it), exactly one capture binding (Escape keydown stays bubble-phase), and the #section-practice-control containment guard (no self-close). A revert to bubble-phase fails the test. 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> |
||
|
|
8fbbc761fc |
fix(v3): reject accidental text-selection of UI chrome (user-select policy) (#637)
* fix(v3): reject accidental text-selection of UI chrome (user-select policy)
Dragging/double-clicking across the v3 UI marquee-highlighted buttons, labels,
the sidebar, transport, and the note-highway HUD — looks broken (reported Mac +
Windows). Default the v3 shell to user-select:none on html, then opt CONTENT
back in. Decided by a 4-lens panel (UX / a11y / dev-ops / plugin-ecosystem);
their guardrails are baked in:
- Form fields ALWAYS re-enabled (input/textarea/select/[contenteditable]) so the
caret + IME composition never break. No `* { user-select:none }` (WebKit input
bug 82692).
- Plugin screens (.screen[id^="plugin-"]) stay selectable BY INHERITANCE (no `*`,
so a plugin's own non-select chrome still wins) — a plugin's copyable text
(lyrics, chords, results), including community/out-of-tree plugins that never
adopt the class, isn't silently locked.
- Core read-only content opts back in by CONTAINER via a hand-authored
`.fb-selectable` (not a Tailwind utility — so runtime-installed plugins get it
too): the whole Settings panel (paths, device names, version, diagnostics,
About) and the now-playing song metadata. Answers the open "keep settings
copyable?" question: yes, at the container.
Cosmetic only — never used to lock copy-worthy text (errors/IDs/paths/versions/
metadata stay selectable; WCAG 2.2 allows copy-paste as a mechanism). v3-only
(v2 unchanged; v3.css loads only on /v3); plain CSS, no Tailwind rebuild; no
desktop/Electron changes (standard OS-framed window). `.fb-selectable` is
documented in CLAUDE.md for plugin authors.
Tests: tests/js/v3_user_select_policy.test.js (html default, form-field
re-enable, plugin-screen carve without `*`, .fb-selectable, container opt-ins,
and the no-`*`-rule guardrail).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF
* fix(v3): address review of the user-select policy (#637)
Review (manual + Codex) of the v3 text-selection policy:
- P1 (real bug): the now-playing HUD metadata opted into `.fb-selectable` but
its `#player-hud` parent is `pointer-events: none`, so the mouse could never
reach the text to select it — the opt-in was inert. Add `pointer-events-auto`
to the metadata block (verified in-browser: user-select:text + pointer-
events:auto, while the HUD parent stays pointer-events:none).
- Coverage: the PR's a11y guardrail promised copyable text stays selectable
"incl. in modals/toasts", but only Settings + the HUD were opted in. Blanket-
opt the focused copyable surfaces back in by selector — `.feedBack-modal`,
`[role="dialog"]`, `#fb-notify-stack`, `#v3-fb-toast`, `#scan-banner` — so
errors / IDs / paths / file names in dialogs, toasts, and the scan banner stay
copyable. These are focused panels, not dense card lists, so re-enabling
selection there can't recreate the across-cards marquee mess.
(Deliberately NOT opting in the library grid / dashboard / profile card lists:
making dense card text selectable would reintroduce exactly that marquee mess
on a drag — copy song metadata from the now-playing HUD / Settings instead.)
- Test (P3): assert the selectable rule's selectors order-independently, cover
the new modal/toast/banner surfaces, and check the HUD block carries BOTH
fb-selectable and pointer-events-auto (class-order independent).
Verified in a real browser (chromium): html=none, sidebar chrome=none, input=
text, Settings=text, HUD meta=text+pointer-events:auto, dialog/modal=text.
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>
|
||
|
|
a791a0d8fe |
feat(v3): DOM-virtualize the Songs grid (#636 item 3 stage 2) (#643)
The v3 Songs grid appended every scrolled page and never released nodes, so card-node count grew unbounded with scroll depth (24 → 624 → 2001 for a 2000-song library). Replace it with a windowed/recycled render: only the visible window (± overscan) is in the DOM while a #v3-songs-gridsizer element sized to ceil(total/cols)*rowH gives the scrollbar full-library geometry; #v3-songs-grid is absolutely positioned to the first visible row. - state.songs is a sparse, absolutely-indexed store filled a page at a time by ensureWindow(): the stage-1 keyset cursor for contiguous forward scroll (O(page)), OFFSET page= for jumps/restore/non-keyset providers. _loadPage shares an in-flight promise per page and an epoch guard discards a stale fetch that lands after a reset. - A–Z rail seeks directly via sort_letters cumulative counts (O(1), no page-through); bounded scan fallback for legacy providers without it. - Snapshot/restore is now scrollTop-based (geometry is stable). Select mode, accuracy badges, ⋮ menu, plugin card actions, and tree/folder coexistence survive cards recycling; renderWindow re-renders when select mode toggles. - Plugins get window.v3Songs.visibleCards() + a v3:library-window-rendered event instead of assuming all cards are present (highway-stutter lesson). Verified in a browser against a seeded 2001-song library: DOM bounded to ~60 nodes while the count reads "2001 songs", rail jump lands on the target row, selection survives recycling, scroll-restore exact. Codex-reviewed (3 findings fixed: stale-fetch epoch guard, await-in-flight page promise, select-mode resync on cached re-entry). Frontend-only. Tests: tests/browser/v3-grid-virtualization.spec.ts pins the bounded-DOM invariant + direct rail jump; tests/js/v3_az_rail.test.js and v3_songs_scroll.test.js updated to the new wiring. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5ed6f454e7 |
feat(library): smart collections as a library provider (#641)
Implements feedBack#636 item 2 (P1) — saved library filters that stay live, the homelab primitive FeedBack was missing (Plex smart collections / Navidrome .nsp / *arr custom filters). A collection is a saved /api/library query surfaced as a registered library provider, so it appears in the v3 source picker and inherits the whole Songs UI (paging, stats, A–Z rail, art) with no new screen. - Storage reuses the playlist subsystem: a `playlists.rules` JSON column (additive, idempotent migration). A row with rules != NULL is a smart collection; list_playlists + get_playlist filter `rules IS NULL`, so collections are excluded from the manual-playlist list and read-only to every playlist mutation that gates on get_playlist. - SmartCollectionProvider (kind="local" — matched songs are local rows, so the client's play/art paths stay on the local branch) delegates query_page/ query_stats/query_artists to the local DB with the stored rules applied; tuning_names/get_art delegate straight through. Registered via a boot scan + on create/update (replace=True) / delete. - Rules mirror the raw /api/library query params; `_sanitize_collection_rules` drops unknown keys and is applied at API ingress AND on provider load, so a hand-edited / imported bad value can't crash a query. - API: GET/POST/PUT/DELETE /api/collections. Frontend: a "+ Save as collection" action in the v3 filter drawer (local provider + active filters only) that names the current filter set and switches to it. Reviewed by Codex; 3 findings fixed (local-kind playback path, save gated to local provider, re-sanitize persisted rules). Tests: tests/test_collections_api.py (CRUD, provider filtering, restart re-registration, kind=local, corrupt-rule tolerance, playlist isolation), tests/js/v3_collections.test.js. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
331857ff2a |
feat(library): keyset cursor pagination + stable sort tiebreak (#636 item 3, stage 1) (#642)
Stage 1 of the virtualized-grid project (got-feedback/feedBack#636 item 3): the data layer the DOM-recycling render window will build on, plus a latent paging bug fixed on the way. - Every grid sort now appends a unique `filename` tiebreak → a TOTAL order. Without it, rows with an equal sort key (e.g. two songs by the same artist) could be skipped or duplicated across OFFSET pages. - query_page gains an opaque `after` keyset cursor: when supplied and the sort can keyset (artist[-desc], title[-desc], recent), the page is fetched with a WHERE-seek instead of OFFSET — O(page), independent of depth. The seek is NULL-aware (NULLs first in ASC / last in DESC) so it's EXACTLY OFFSET- equivalent; the legacy `dir=desc` shape is canonicalized so its cursor seeks the right direction. Unknown/compound sorts + bad cursors fall back to OFFSET. - /api/library exposes `after` + `next_cursor`. Only the true local provider is handed a cursor (a collection may pin a different sort; remote don't keyset), so both page by OFFSET safely. - Composite (artist NOCASE, filename) / (title NOCASE, filename) / (mtime, filename) indexes cover the order; `after` added to the optional provider kwargs so legacy providers drop it. Codex-reviewed; 3 findings fixed (dir=desc canonicalization, NULL-key seek, cursor only for the local provider). Tests: tests/test_library_keyset.py (keyset==OFFSET parity for 5 sorts, stable tiebreak on equal keys, dir=desc, NULL sort keys, bad-cursor + compound-sort fallback). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8ca7ea4002 |
feat(library): persisted wishlist / "wanted" list (#640)
Closes feedBack#636 item 4 — the *arr "Wanted/Monitored" analogue FeedBack was missing. A wishlist entry is a song the user does NOT own yet, so unlike a playlist (which references owned local songs by filename) it can't reuse the playlist subsystem; it lives in a new `wanted` table keyed by descriptive identity (artist, title, source, source_ref, note, created_at). - New table + a UNIQUE index on (artist NOCASE, title NOCASE, source, source_ref); additive + idempotent (CREATE … IF NOT EXISTS). - MetadataDB.add_wanted (INSERT OR IGNORE + re-select under the write lock, so a re-run of an ownership-diff returns the existing row, never a dup), list_wanted (newest first), remove_wanted, count_wanted. - Routes GET/POST/DELETE /api/wanted. POST requires artist or title and defaults source to "manual"; idempotent on identity so producers (the find_more ownership-diff, or a manual add) can re-post freely. This is the core persistence primitive the charrette flagged as the missing piece; the consuming UI lives in the producing plugin (find_more / the_daily). Tests: tests/test_wanted_api.py (round-trip, identity idempotency incl. case-insensitive, distinct source_ref, ordering, validation, additive schema). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
07ab902604 |
feat(settings): back up the library DB + custom art in the export bundle (#639)
Closes the dev-ops lens's #1 finding from the library charrette (got-feedback/feedBack#636 item 1): scores, favorites, playlists, and play history — the only library state a rescan can't rebuild — were absent from the settings backup. Now GET /api/settings/export carries an additive `core_server_files` section: - a CONSISTENT snapshot of web_library.db via the SQLite online-backup API (a complete single file even while the server runs; taken under the MetadataDB write lock), base64-encoded; - custom playlist covers + avatar (CONFIG_DIR/playlist_covers, /avatars), walked with the existing _walk_export_paths machinery. Restore is DB-safe: - POST /api/settings/import STAGES the DB to web_library.db.restore (never over the live, open file); _apply_pending_db_restore swaps it in at the next startup BEFORE the connection opens, clearing stale -wal/-shm so a stale WAL can't be replayed onto the restored file. Response sets `restart_required` + a warning; custom art applies immediately. - The staged DB is integrity-checked (open + PRAGMA quick_check) at import AND again at startup before the live DB is touched — a corrupt/truncated restore is refused/discarded and the live DB is left intact, so a bad bundle can never brick startup or lose data. - Export hard-fails (500) if the snapshot can't be produced (no silent DB-less backup); a partial import disarms its own staged restore. Backward-compatible: older servers ignore the new section; a bundle without it imports as before. Known gap: custom uploaded *song* art is still commingled with the rebuildable thumbnail cache in art_cache/, so it isn't bundled yet (tracked follow-up on #636). Tests: tests/test_settings_export_library_db.py (snapshot consistency, staged-not-live restore, sidecar clearing, corrupt-DB refusal at import + startard, traversal rejection, export hard-fail, disarm-on-failure, full round-trip). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6a6efc793a |
feat(v3 library): practice-aware home — Repertoire meter + "Keep practicing" shelf (#635)
* feat(v3 library): practice-aware home — Repertoire meter + "Keep practicing" shelf The Songs page opened cold into a flat sorted grid. This adds a practice-aware front door on the unfiltered grid, built entirely from data already on hand (no new endpoints, no new stored state): - Repertoire meter — "Repertoire: N of M songs · K in progress" + a bar, counting songs at/above the same mastery threshold the green accuracy badge uses (>= 0.9 best accuracy) over the unfiltered library total. Reads state.accuracy (/api/stats/best, already loaded for the card badges) and the unfiltered /api/library/stats total. - "Keep practicing" shelf — a horizontal row of recently-played, not-yet- mastered songs (newest first, click to play). Reads /api/stats/recent. Both show ONLY on the grid view when not searching/filtering/selecting (the front-door context), refresh after a song is scored (applyScoreRefresh), and collapse on an empty library. Soft-gamification only: descriptive encouragement (goal-gradient / endowed-progress), never content-gating, decay, or nagging — the practice-accuracy "continue" rail a media server can't do. Frontend-only: static/v3/songs.js (renderLibraryHome / _repertoireCounts / libHomeVisible, wired through reload() + applyScoreRefresh), static/v3/v3.css. Came out of the library design charrette (UX + gamification lenses' top pick). Stacked on the A–Z rail branch (feat/v3-library-az-rail) since both touch static/v3/songs.js; merge that PR first (or retarget). Tests: tests/js/v3_keep_practicing.test.js (threshold, front-door gating, shelf filter, denominator, render/reload/score-refresh wiring, click-to-play). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(v3 library): correct practice-aware home for review P1/P2/P3 Addresses the PR #635 review findings (manual + Codex): P1 correctness - Gate the Repertoire meter + "Keep practicing" shelf to the LOCAL provider (libHomeVisible). They read local practice stats (state.accuracy / /api/stats/recent); on a remote provider they mixed a local mastered count with a remote song total (e.g. "85 of 80") and the shelf played local files while browsing a remote library. - Shelf now gates on the per-SONG best (state.accuracy[filename] = MAX across arrangements, what the green badge shows) and dedupes by filename, instead of the per-arrangement recents row — so a "keep practicing" card can no longer show a green "mastered" badge, and a song can't appear twice. P2 robustness - renderLibraryHome fetches /api/library/stats + /api/stats/recent together (Promise.all) and a _homeToken generation guard discards a stale render so a slow response can't repaint a home the grid already moved past. P3 polish - accuracyBadge references MASTERY_ACCURACY instead of a bare 0.9, so the badge and the meter/shelf can't drift from "the same mastery threshold". Tests updated (v3_keep_practicing.test.js): provider gating, per-song deduped shelf, Promise.all + token. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
6a71577e05 |
feat(v3 library): A–Z fast-scroll jump rail on the Songs grid (#634)
* feat(v3 library): A–Z fast-scroll jump rail on the Songs grid Adds a vertical letter rail (Plex/Radarr/iOS-contacts pattern) pinned to the right edge next to the scrollbar so you can jump the library to a starting letter — tap, drag-to-scrub with a live letter bubble, or arrow-key between letters. The classic (v2) tree already had letter selection; this brings the new v3 grid to parity (it was the gap behind the "alphabetical scroll selection next to the scrollbar" idea). It shows ONLY for the grid view + alphabetical (artist/title) sorts, and only offers letters present in the current sort AND filter set, so a tap always lands on a real card (absent letters are dimmed + non-interactive). The grid is forward-only, server-paged infinite scroll with no virtualization, so a jump pages through to the target card then scrolls to it; a token guards overlapping jumps (drag) so the newest wins. A keyset-seek + virtualized window is the scaling follow-up for very large libraries. Backend: /api/library/stats gains an optional `sort` param and an additive `sort_letters` map — songs-per-first-letter of the ACTIVE sort column (artist or title), filter-synced — so the rail's present-letters match the grid's real order. The legacy `letters` (distinct-artist) field is unchanged, so the dashboard + classic tree are unaffected. `sort` is dropped for providers whose query_stats predates it (existing kwarg-filter), so third-party library providers keep working (rail simply falls back / hides). Frontend: static/v3/songs.js (refreshRail / jumpToLetter / pointer-drag + keyboard, cards tagged data-letter), static/v3/v3.css (.v3-azrail + bubble). Tests: tests/test_library_filters.py (sort_letters artist/title, song-vs- distinct-artist counting), tests/test_library_providers.py (sort forwarded), tests/js/v3_az_rail.test.js (gating, data-letter, load-through, drag/keys). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(v3 library): harden A–Z jump rail (review P2/P3) Addresses the PR #634 review findings (manual + Codex): P2 correctness - refreshRail prefers the active-sort `sort_letters`; falls back to the artist-based `letters` only on an artist sort, and hides the rail on a title sort when a legacy provider returns none (was mislabeling letters). - reload() bumps `_jumpToken` so an in-flight letter jump can't scroll a grid that's being rebuilt from page 0. - songBucket no longer trims, matching the server SQL + grid ORDER BY raw first-char bucketing (a leading-space title now buckets under '#' on both sides). P3 polish - Paging guard is total-derived (ceil(total/PAGE_SIZE)+2) instead of a magic 4000, keeping large libraries reachable while still bounded. - Roving tabindex: only the first present letter is tabbable; arrow keys move it. Removes up to 27 page tab stops. - `sort_letters` is computed only when the caller opts in (want_sort_letters / route `sort_letters=1`); the dashboard + v2 tree skip the extra GROUP BY. Added sort + want_sort_letters to the optional provider-kwargs so non-introspectable legacy providers drop them. - _railToken supersedes stale refreshRail responses; hide the rail when no letters are present instead of rendering disabled buttons. Tests updated accordingly (v3_az_rail.test.js, test_library_filters.py). 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> |
||
|
|
b29bab1884 |
fix(highway_3d): keep the FPS counter from hiding behind the v3 "Up Next" pill (#630)
The on-highway FPS readout (Settings -> Graphics -> 3D Highway -> Show FPS counter) is pinned to the top-right of the highway overlay -- the same corner the v3 player chrome stacks its persistent Up Next pill and live-performance HUD into, on a higher layer that paints over the canvas. So the readout sat behind that chrome and couldn't be read, exactly when a tester turned it on to judge performance (and because the pill is default-on it covered the counter regardless of the separate "Up Next won't turn off" report). Keep it top-right (where testers look) but drop it just below whichever of that chrome is showing: measure the lowest visible top-right v3 HUD element (#v3-upnext / #v3-live-performance-hud / #hud-time) and floor the FPS box's Y beneath it. Element refs are resolved once and cached (no per-frame querySelector, per the plugin perf rules) and only read while the counter is actually drawn; gated on window.feedBack.uiVersion === 'v3' so classic v2 is byte-for-byte unaffected. Bump plugin version 3.30.0 -> 3.30.1 (the screen.js cache-buster). Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
271fedda55 |
fix(input_setup): stop collapsing audio driver-type variants in the wizard (#627)
The onboarding audio picker de-duped the device list by display LABEL. On
Windows the engine enumerates one interface once per host API (ASIO /
Windows Audio / DirectSound) with the same name, so the variants collapsed
to a single choice — silently keeping whichever sorted first, often not the
low-latency ASIO one the player wants. It could also drop the variant that
was actually `selected`.
The audio-input capability already collapses true duplicates by
logicalSourceKey (_visibleInputSources), and these variants each have a
DISTINCT key, so the wizard's extra label-collapse was redundant for real
dupes and destructive for the variants. Removed it; the picker now lists
every selectable input.
Pairs with feedBack-desktop's change to tag each source label with its
driver type ("Focusrite (ASIO)" vs "(Windows Audio)") so the now-distinct
entries are legible.
Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
90fb2ee3bc |
feat(v3): content-dependent playlist covers + custom art (#626)
* feat(v3): content-dependent playlist covers + custom art upload Playlist cards were a tiny 🎵 emoji on an empty square. Now the cover reflects the playlist's contents, and you can override it with a custom image. Cover (in priority order): - custom uploaded cover, else - empty playlist -> the icon - a few songs -> the first song's album art - 4+ songs -> a 2x2 album-art mosaic Backend (server.py): - MetadataDB.list_playlists() returns each playlist's first few still-present songs' art URLs (`art_urls`) for the content cover. - GET /api/playlists and GET /api/playlists/{id} add `cover_url` when a custom cover exists. - POST/GET/DELETE /api/playlists/{id}/cover — store a small PNG thumbnail under CONFIG_DIR/playlist_covers/ (PIL-converted, mirroring song-art upload); the cover is deleted with the playlist. Cover mutators added to _MUTATING_ROUTES. Frontend (static/v3/playlists.js): playlistCoverHtml(p) renders the rules above; the playlist detail view gets "Cover" (pick an image) + "Remove cover". Tests: tests/test_playlists_api.py (art_urls + cover roundtrip / reject-non-image / delete-removes-cover — 11 pass) and tests/js/v3_playlist_cover.test.js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(playlists): 400 (not 500) on non-string cover image + bust same-second cover cache Two review follow-ups on the playlist-cover endpoints: - POST /cover did `if "," in b64` before any type check, so a non-string image (e.g. {"image": 123} / null) raised TypeError -> 500. Guard with isinstance (mirrors the avatar/song-art upload) for a clean 400. +regression test covering number/null/object/list. - The cover URL busted only on int(st_mtime) (1s granularity) and GET /cover sent no cache headers, so a same-second replace/remove/re-upload could serve a stale image. Use st_mtime_ns in the cache-bust token and add the shared no-cache header (_ART_CACHE_HEADERS), matching song art. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
3d97c07b2b |
feat(v3): add "Add to playlist" to a song's ⋮ More menu (#625)
* feat(v3): add "Add to playlist" to a song's ⋮ More menu You could only add a song to a playlist via select-mode (checkbox → batch bar). Add an "Add to playlist" row to each song card's ⋮ overflow menu that targets that one song, reusing the same picker (pick a listed number or type a new name to create the playlist). The select-mode batch flow and the single-song menu now share one extracted `addFilenamesToPlaylist(filenames)` helper; the menu is `openCardMenu`, shared by grid cards and tree rows, so both views get it. Tests: tests/js/v3_add_to_playlist_menu.test.js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(v3): don't clear the batch selection when the playlist picker is cancelled The extract-helper refactor made batchAddToPlaylist() call finishBatch() unconditionally, so cancelling (or a failed create) cleared the multi-select and reloaded the grid — a regression from the original early-return-on-cancel behaviour. addFilenamesToPlaylist() already returns null on cancel/failure; gate finishBatch() on a truthy playlist id so the selection is preserved for a retry. Adds a regression assertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
b103a722ce |
fix(v3): refresh Songs grid after a Settings rescan / DLC-folder change (#624)
Reported on macOS: on a fresh install, pointing at a DLC folder in Settings and running a scan showed NO songs until an app restart. The scan itself was fine — _background_scan re-reads config.json fresh, so it scans the new folder and populates the library — but the v3 Songs grid never reloaded. The Settings Rescan / Full Rescan handlers only refreshed the classic (v2) library via loadLibrary(); the v3 grid (static/v3/songs.js) had no listener for a scan it didn't initiate (only its own upload path self-refreshes via watchUploadScan). So its cached, pre-DLC (empty) DOM/snapshot survived a sidebar return until a full reload (restart). Fix: the rescan handlers now emit `library:changed` (static/app.js). The v3 grid listens and reloads if it's the active screen, else sets `_libraryDirty` so the next onV3SongsScreenEnter does a full re-fetch — a short-circuit placed ahead of every cached-DOM fast-path so it can't restore the stale grid. Tests: tests/js/v3_library_refresh.test.js guards the emit + the reload/dirty wiring (DOM/event glue isn't headlessly unit-testable; end-to-end wants an in-app run of the reporter's flow: set DLC in Settings → scan → Songs populate without restart). Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
d841813e0b |
fix(library): Edit Metadata modal — editable Year + don't close on drag-release outside (#623)
* fix(library): Edit Metadata modal — editable Year + no close on drag-release Two fixes to the Songs -> Edit Metadata modal (openEditModal/saveEditModal in static/app.js), both reported on macOS for 0.3.0. 1) Year is now editable. A year can be set when authoring a pak but the modal had no Year field, so it could never be changed. The backend (POST /api/song/<f>/meta) already accepts + normalizes `year` and writes it into the file via songmeta (survives a rescan) -- only the UI omitted it. Add a Year input (populated from the song's current year) and include `year` in the save POST body. Both the v3 card menu and the legacy edit button already pass the year through, so both surfaces get the field. 2) The modal no longer closes when a click-drag is released on the backdrop. Selecting text inside a field and releasing the mouse past the modal edge dismissed the form without warning (the `click` event's target resolves to the backdrop, the common ancestor) -- discarding the edit. Backdrop dismissal now also requires the mousedown to have STARTED on the backdrop, tracked per-modal and decided by a new pure helper _editModalShouldClose(clickTarget, modalEl, downOnBackdrop). Cancel / X still close on a normal click. Tests: tests/js/edit_metadata_modal.test.js extracts the real functions from app.js and asserts (a) openEditModal renders #edit-year, (b) saveEditModal's meta POST body carries `year`, and (c) the backdrop-close decision table (Cancel always closes; backdrop needs down+up on the backdrop; a drag from a field released on the backdrop does NOT close). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(library): wire Edit Metadata Save via listener, not an inline onclick encodeURIComponent does not escape "'", so embedding the filename in the single-quoted inline onclick="saveEditModal('…')" handler produced a malformed handler for any song whose filename contains an apostrophe (e.g. Bob's Song.sloppak) — clicking Save threw a syntax error and the edit silently failed. Replace the inline onclick with a data-edit-save hook wired in JS from the closure filename (mirrors the existing Delete button pattern), so the filename never has to survive attribute-string embedding. Pre-existing bug surfaced during review of this modal. Adds a regression assertion (no inline saveEditModal onclick; Save wired via data-edit-save). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
a0f5435854 |
perf(highway): stop the adaptive renderScale from visibly hunting up/down (#622)
Alpha testers reported the 3D-highway "quality going up and down to try to compensate" as passages got busier (#618 charrette). That's core's load-adaptive render scale (_adaptRenderScale, #654) ping-ponging across the 7-12ms deadband: it downscales when a busy frame blows the budget, then the now-cheaper frame dips under the low watermark so it upscales, which blows the budget again — a visible resolution pop on a loop. Fix: keep downscaling prompt (protect the frame rate), but make UPSCALING lazy and predictive: - smaller up-step (x1.06 vs x1.1) on a longer, separate cooldown (_AUTO_UPSCALE_COOLDOWN_MS = 2500ms vs the 600ms general adjust cooldown), reset on any downscale so we never bounce straight back up; - a predictive guard: only upscale when the projected cost AFTER the step (~cost * step^2, since draw cost tracks pixel count) still clears the high budget. The scale settles just inside the deadband instead of oscillating. No public API change; the user-facing "Min res" floor (_autoScaleMin) is untouched. Pairs with the in-plugin AA-under-bloom fix in feedBack#618. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
2a43d5b494 |
fix(diagnostics): rebuild diagnostic sloppak so song name reads "FeedBack", not "Slopsmith" (#621)
PR #586 renamed the bundled diagnostic to docs/diagnostics/feedBack-diagnostic-basic-guitar.sloppak but only git-mv'd the file -- it never regenerated the zip. So the manifest INSIDE still carried `title: Slopsmith Diagnostic — Basic Guitar` / `artist: Slopsmith` (and the same heading in DIAGNOSTIC.md), which is the name testers saw in the library/player and the onboarding calibration step. Meanwhile the build script, server `_BUILTIN_DIAGNOSTIC_SOURCES`, README, and docs all already say "FeedBack Diagnostic — Basic Guitar" -- only the committed binary was stale. Regenerate the artifact from its own generator (docs/diagnostics/build_diagnostic_basic_guitar.py) so the committed sloppak matches the source of truth: title/artist/heading now "FeedBack"; the chart (5 notes / 7 chords / 5 sections), the click-track stem, and the `diagnostic:` metadata block are unchanged. Verified the rebuilt manifest parses, carries a real U+2014 em-dash, and contains no "Slopsmith". No code change -- the #586 rename just needed the rebuild it skipped. 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> |
||
|
|
ee7bafbb47 |
fix(v3): decode stats:recorded filename so post-play score badge refreshes (#620)
PR #574 added a `stats:recorded` -> in-place accuracy-badge repaint so a just-earned score shows without restarting the app. But the repaint never matched a card, so the badge stayed stale until a full render() (app restart / search / re-enter the screen) -- exactly the "only updates after a restart" report. Root cause is a filename key-space mismatch. The event (like song:loading) carries the filename `encodeURIComponent`'d, because that is what playCard hands to playSong (the highway WS decodeURIComponent's it). Library cards, though, key on the DECODED localFilename (data-fn), and /api/stats/best is server-canonicalized to that same decoded key (server.py _canonical_song_filename). So repaintAccuracy's `data-fn !== key` check rejected every card and `state.accuracy[encoded]` was undefined. Decode the event filename back into the card / state.accuracy key space via a small `decFn` helper before marking dirty and repainting, fixing both the immediate repaint and the onV3SongsScreenEnter deferred path. decFn is idempotent for already-decoded names and falls back to the original on malformed input, so a real filename containing a literal '%' is never corrupted. Tests: tests/js/v3_songs_score_badge_refresh.test.js extracts the real decFn from the shipped source and proves the encoded event filename round-trips to the raw card key (incl. spaces and subfolder '/'). Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
fef870047b |
fix(player): reliable Escape "Back" + resumable, optionally-confirmed song exit (#619)
* fix(player): make Escape a reliable Back; resumable + optionally-confirmed song exit Escape didn't always leave a song: clicking a transport control (play/FF/RW/ restart) left that <button> focused, and _shortcutDispatchBlocked() bails the shortcut dispatcher for any focused INPUT/SELECT/TEXTAREA/BUTTON — so the player-scope Escape=Back shortcut never fired until the user clicked empty canvas to blur the control. Space already had a player-screen carve-out (#593); Escape did not. That asymmetry was the bug. Phase 1 — focus fix: generalize the Space carve-out in _shortcutDispatchBlocked to Escape, on the player AND settings screens (both register Escape=Back; settings had the identical latent bug). The earlier guards still win: text inputs are exempted first, the Section Practice popover already claims Escape, and a true modal (role=dialog aria-modal=true / .feedBack-modal) still traps it. Plugins' player-scope Escape shortcuts are fixed identically. Phase 2 — resume: leaving the player snapshots {song, arrangement, position, speed} to localStorage; a non-blocking "Resume practice" pill offers it back on the next non-player screen / next launch. playSong() gains a {resume} option that restores speed + seeks to the saved position on song:ready instead of the normal autostart. Conservative (ignores <3s / near-end), cleared on natural song-end and once consumed, expires after 24h. Phase 3 — opt-in "Ask before leaving a song" (Gameplay tab, default OFF). A true-modal confirm with monotonic Escape (the second Escape leaves) and Space/Enter = Leave. The player Escape shortcut and the v3 close button route through window.requestExitSong(); auto-exit on song-end and a results screen's own Close stay unguarded. Design rationale: a multi-seat design charrette (engagement, learning-design, operability, codebase-reality) — leaving a song should be reliable and recoverable, not gated; the confirm is opt-in only. Tests: tests/browser/{keyboard-shortcuts,resume-session,exit-confirm}.spec.ts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * test(browser): suppress first-run onboarding in keyboard/resume/exit specs The first-run onboarding overlay (#v3-onboarding) is a modal that intercepts pointer/keyboard events; on a fresh profile it covers the player and breaks any test that presses Escape or clicks. Stub GET /api/profile to an onboarded profile in each beforeEach so the app behaves like a returning user (the state these tests assume). Also tighten the Section Practice Escape test to assert the guarantee the fix actually provides — Escape does not exit the song while the popover is open (the line-447 guard wins over the carve-out) — rather than asserting the popover's own close handler fires, which isn't wired for a synthetic bar. Verified locally against a worktree server (Chromium): all 16 new specs pass (5 Escape + 6 resume + 5 exit-confirm) plus the existing #593 Space tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(player): exit-confirm — Escape cancels back to song, pause on open/resume on stay Refinements from tester feedback on the exit-confirm (default stays OFF): - Escape on the open prompt now = Stay (dismiss + return to the song), matching every other modal and the generic _confirmDialog (Esc=cancel). A second Escape therefore returns to the song instead of leaving it. Leaving stays the explicit, default-focused "Leave" button, so Space/Enter/click = "just get me out" (the OP's "Space always hits leave"). - Opening the prompt PAUSES the song (via the canonical togglePlay path, HTML5 + _juceMode) so it isn't running/being scored behind the modal; Stay resumes exactly what we paused. Guards: cancel any count-in on open; resume only if we paused (wasPlaying), only if still the same live song on the player (_audioSeekGen unchanged), and never auto-resume a song the user had paused. - Trap Tab inside the dialog; backdrop click was already Stay. Specs: exit-confirm.spec.ts updated — the monotonic "second Escape leaves" test becomes "second Escape stays", plus a backdrop-click-stays test. The audio pause/resume itself is verified manually on web + desktop (the mock song has no backing track); these specs lock the navigation + keyboard semantics. NOTE: the pause/resume adds a new pause→resume cycle on the desktop JUCE transport (known play/pause-desync path) — smoke-test on the desktop build before merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(player): accurate exit-confirm copy + keep resume snapshot on failed load Two review follow-ups on the Escape/resume/confirm work: - Settings copy said "a second Escape (or Space/Enter) still leaves", but Escape dismisses the confirm (Stay) like every other modal — only Space/Enter/Leave exit. Corrected the Gameplay-tab description so it matches the implementation (and the committed exit-confirm specs). - resumeLastSession() cleared the snapshot BEFORE awaiting playSong(), so a transient load/connect failure permanently lost the Resume pill with no retry. Clear only after the load resolves; on failure keep the snapshot (and drop the pending in-memory resume) so the pill re-offers it on the next non-player screen. All 16 Escape/resume/exit-confirm Playwright specs still pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
3b2d83d406 |
feat(folder_library): Folder Library core plugin (#610)
Adds the bundled Folder Library plugin (browse the DLC library by its on-disk folder tree, in-app folder CRUD, drag-and-drop + dialog song moves, sort/filter, live search), wired into the classic v2 toolbar and the v3 Songs page. Includes the screen.js IIFE dedup (unified surface factory) and review fixes: path-traversal guard on /song/move, folder-delete data-loss fix, plural /api/plugins/<id> namespace, loose-folder song recognition, error-text escaping, v3 setLibView null-guard, and tests. Co-authored-by: Kyle <kyle.j.t@live.co.uk> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d1f7f12293 |
fix(v3): add "Show 'Up Next'" toggle so the player pill can be turned off (#612)
The v0.3.0 player chrome's persistent upcoming-section pill (#v3-upnext, drawn by static/v3/player-chrome.js's updateUpNext) shipped with no off switch: it always showed during playback whenever a section was upcoming, overlapping the top-right FPS HUD and ignoring the 3D-highway "Show 'Up Next' section card" checkbox (a different, in-canvas widget demoted to default-off precisely because this pill is the canonical readout). Users reading the pill as that same setting saw "disabled in settings but still there." Add a real core toggle, following the autoplayExit idiom: - static/app.js: client-only `showUpNext` localStorage pref (absence = enabled), _showUpNextEnabled()/setShowUpNext(), loadSettings() hydration, and a read-only window.feedBack.showUpNext getter. Disabling mid-playback hides the pill immediately. - static/v3/index.html: a "Show 'Up Next'" switch in the Gameplay tab. - static/v3/player-chrome.js: gate updateUpNext() on the pref. - static/v3/settings.js: add showUpNext to RESET_MAP.gameplay.local. Default ON, so behaviour is unchanged for existing users. v3-only (the pill is v3 core chrome); no Tailwind rebuild. Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4c3ec2ff66 |
feat(plugins): full-screen (immersive) plugin screens via manifest opt-in (#590)
DAW-style plugin UIs (e.g. a practice studio) need the whole viewport, not a scrolling content page below the v3 topbar — embedded in the shell they get cut off at the bottom with excess padding up top. Add an opt-in top-level `"fullscreen": true` plugin.json field, surfaced as the `fullscreen` boolean on /api/plugins (mirrors the settings_category plumbing in plugins/__init__.py). When a fullscreen plugin's screen is active, static/v3/ shell.js toggles `html.fb-immersive` from syncActive() so it tracks every navigation incl. deep-link; static/v3/v3.css then hides the topbar, collapses the sidebar to a functional icon rail (kept reachable — Escape is bound only on player/settings scopes, so a fully hidden sidebar would trap the user), and lets the active plugin screen fill #v3-main. Mirrors the existing ss-follower-pre chrome-hide pattern. Additive + opt-in: plugins without the flag are unaffected. Test: tests/test_plugins.py::test_fullscreen_flag_parsed_from_manifest Claude-Session: https://claude.ai/code/session_01BmWopMsRjdZyD6RwmZAQBv Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
8b4c9b0050 |
Fix list/tree view: select mode, parts visibility, song actions (#585)
* Fix list/tree view: select mode, parts visibility, song actions Bring the v3 list/tree view to parity with the grid card: - Select mode now renders a per-row checkbox + selected-ring, preserves expanded artist groups across re-render, and a capture-phase guard makes a row/chip click select the song instead of starting playback. - Always-on favourite / save-for-later / overflow-menu cluster on each row, same actions as the grid card. Rebuild static/tailwind.min.css so the new utilities are compiled in - notably .sm:flex behind the arrangement chips' "hidden sm:flex" wrapper. Without it the chips (and #582's badges) render display:none on the Docker build, which serves the committed CSS; the desktop build looked fine only because it rebuilds Tailwind from source at bundle time. Signed-off-by: Sin <deathlysin@outlook.com> * fix(v3): regenerate tailwind.min.css from source + add tree select tests + CHANGELOG The committed tailwind.min.css was over-built: 135,578 bytes / 1,428 selectors, with 294 selectors (accent-amber-400, bg-cyan-500, animate-spin, after:bg-gray-400, …) used in zero core source files — bloat from a local build scanning outside the repo's content globs. It would fail CI's rebuild-and-diff and violates the byte-stable rule in scripts/build-tailwind.sh. Regenerate via `scripts/build-tailwind.sh` (pinned tailwindcss@3.4.19): 111,491 bytes / 1,134 selectors, byte-identical to a clean rebuild, still containing the .sm\:flex fix plus every new tree class (ring-fb-primary, accent-fb-primary, pointer-events-none, …). Docker chips now render and CI stays green. Add tests/browser/v3-tree-select.spec.ts: - select mode keeps expanded artist groups open across the tree re-render (fails without loadTree's openArtists capture/restore) - clicking a row in select mode selects instead of playing Record the fix under CHANGELOG [Unreleased] -> Fixed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Sin <deathlysin@outlook.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
82db8e56b1 |
chore(hotkeys): remove sloppak-convert library hotkey (#594)
* chore(hotkeys): remove sloppak-convert library hotkey
Removes the 'c' keyboard shortcut for converting library entries to
.sloppak. The shortcut was defined in two places:
- The no-op registerShortcut() entry that only existed to show in the
? help panel (the Sloppak Converter plugin handles conversion and
can register its own shortcut via window.registerShortcut).
- The c dispatch in the library-entry keydown handler
({ c: 'button.sloppak-convert-btn', ... }) that triggered the
plugin button.
* test+docs: update tests & CHANGELOG for removed `c` convert hotkey
The previous commit removed the `c` library hotkey but left three
assertions in tests/browser/keyboard-shortcuts.spec.ts that require it,
which fail deterministically (the two registry tests read window._panels
directly, independent of environment):
- should list all registered shortcuts (required {key:'c',scope:'library'})
- should have correct shortcut scopes (expected library::c)
- should show library shortcuts in help modal (Convert library entry / c)
Drop those assertions and record the removal under CHANGELOG
[Unreleased] -> Removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: byrongamatos <xasiklas@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|