mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-22 12:52:29 +00:00
feat/v3-play-queue
116 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
df367e5cce |
feat(v3): play a playlist as a queue with auto-advance
Playing a playlist previously played one song and returned to the menu -- a play-queue was never implemented. Add window.feedBack.playQueue (start / advance / hasNext / clear) and a "Play all" button on the playlist detail. Advancing rides the same exit choke point as auto-exit and a results-card close: song-end paths call window.closeCurrentSong() (the auto-exit grace timer and a results screen's release()), so wrapping it plays the next track instead of returning to the menu -- advancing AFTER the user dismisses a score card, not through it. A user-initiated exit (Escape / the close button) uses the bareword closeCurrentSong(), left untouched, so leaving the player still leaves and abandons the queue. playSong gains a fromQueue guard (a manual play abandons a stale queue) and closeCurrentSong clears the queue on a real close. Binds via song:ended / the choke point, not the <audio> element, so it advances on the desktop (JUCE) route too. The no-queue path is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF |
||
|
|
f9607c5c94
|
Port the Min res (minimum auto-resolution) selector into the v3 player (#663)
The v2 control bar exposes a "Min res" selector next to Quality that sets highway.setMinRenderScale — capping how far the load-adaptive resolution scaler (feedBack#654) may downscale, or disabling it entirely (Full). The v3 UI only ported the Quality selector, so v3 users had no way to stop the highway auto-downscaling to as low as quarter-res on heavy scenes / weak- GPU launches — pixelated even at Quality = HD, with no workaround (worse than v2). Add the Min res row to the v3 viz/quality rail popover, under Quality, mirroring the v2 control (same options, handler, title, aria). The handler, the setMinRenderScale/getMinRenderScale API, and the shared app.js init that syncs the selector's value (guarded by element id) all already exist — only the v3 markup was missing. Fixes #662 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3e3a98a0d0
|
Aspect-aware framing for ultra-wide 3D highway panes (#652)
* Add aspect-aware framing for ultra-wide highway panes
On a top/bottom 2-player split each 3D highway pane is full-width /
half-height (~32:9). The camera's vertical FOV was locked at a single
value, so at that aspect the horizontal cone ballooned past 130deg and
squeezed the fixed-width neck into a thin central sliver with large dead
margins on either side.
Add a "horizontal-FOV-hold" path: past a configurable start aspect the
effective vertical FOV is lowered so the horizontal cone stays roughly
constant, letting the neck fill a wide pane. At/under the start aspect it
is an exact no-op, so normal ~16:9 single-player and most 2x2 panes are
unchanged. Optional pose nudges (height / dolly / pitch / look-depth)
further flatten the view toward a low, immersive angle.
Everything is driven by a runtime bridge (window.__h3dAspectTune) with a
live tuner panel (Shift+A in the player) exposing every knob plus a live
aspect/FOV readout, localStorage persistence, and a Copy button. Toggling
the feature off restores the exact prior framing, so it doubles as an A/B
control. Shipped on by default for wide panes for testing feedback.
Source-pinned by tests/js/highway_3d_wide_fov.test.js.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>
* fix(highway_3d): ship wide-pane framing default-OFF with a coherent config
Review fixes for the aspect-aware framing. The first cut shipped
_ASPECT_DEFAULTS = { enabled:true, baseVfov:30, blend:0, minVfovDeg:36 },
which contradicted the PR's own "default off → byte-for-byte prior behaviour"
claim:
- enabled:true made the tune active for everyone, and baseVfov:30 forced every
pane's vertical fov from 70° to 30° (normal single-player/2x2 panes included —
a drastic global zoom, not the advertised no-op).
- blend:0 collapsed the Hor+ math back to base, so the actual horizontal-FOV-
hold did nothing even on wide panes — the only net effect was the zoom.
- minVfovDeg:36 > baseVfov:30 was an inverted floor (clamped wide panes UP to
36° rather than flooring a real reduction).
New defaults: { enabled:false, baseVfov:BASE_VFOV(70), blend:1,
minVfovDeg:HORPLUS_MIN_VFOV(28) }. Now:
- OFF by default → camUpdate passes a null tune → effectiveVfov returns
BASE_VFOV → exact no-op on every pane (verified: 70° at 16:9 and 32:9).
- When a tester enables it (Shift+A), baseVfov==BASE_VFOV keeps normal/≤start
panes at 70° (still a no-op there) and blend:1 makes the hold actually engage
on genuinely wide panes (47.7° at 32:9, flooring toward 28° as aspect grows).
- minVfovDeg < baseVfov is a real floor.
Also bumps the localStorage key (h3d_aspect_tune → h3d_aspect_tune2) so a
machine that persisted the old broken default gets the corrected one, and adds
source-pin tests guarding default-off + the coherent base/blend/floor so this
can't silently regress to default-on again. The pose-nudge values are left as
the author's in-progress wide-pane look (dormant until enabled). 110/110 tests
pass; node --check clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: topkoa <topkoa@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
|
||
|
|
db81d7dafb
|
Add progress bar to v3 "Up Next" pill (#649)
The persistent top-right "Up Next" pill showed the upcoming section name
and a countdown ("in 12.3s") but no at-a-glance sense of how far through
the current section the song is. Add a thin progress bar directly under
the existing text that fills as the current section elapses toward the
next, reaching full when the section flips.
The text row is wrapped unchanged in a flex row and the pill stacks the
bar beneath it; nothing else about the pill's content or styling changes.
Progress is computed in updateUpNext() as the fraction elapsed between the
previous section boundary (last section at/before now, else song start)
and the next section. The fill uses the same gradient as the section name
for visual cohesion.
Signed-off-by: topkoa <topkoa@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
a732e1f9d2
|
perf(tuner): idle the always-on tuner viz rAF when there's no signal (#647)
The v3 home tuner card runs continuously, and every tuner visualization drove a self-rescheduling 60fps requestAnimationFrame loop that never stopped — pinning a renderer core even on a silent home screen with the needle/strobe at rest. Make each viz idle its loop once there's nothing left to animate, and re-kick it from update() on the next reading that actually moves it: - analogue-gauge: stop when the needle + drum strip have settled on their targets (|target-current| below a sub-visible epsilon); restart when a new reading moves the target. - strobe / mace-fx-iii / chef-mt3: stop when there's no live signal and the strobe drift (and glow fade) have fully decayed; restart on the next note. - toilet-tuner: stop when silent and the plunger has eased back to centre; restart on the next reading (guarded so repeated no-signal updates don't re-kick a parked loop). Active tuning is unchanged — the loop runs whenever a note is sounding or the indicator is still moving. Bumps tuner 1.3.1 -> 1.3.2. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dfa825b4ab
|
docs: host theme contract proposal (#645)
* docs: host theme contract proposal (prevent features carving into one theme) Charrette output after a plugin UI feature (note_detect results card) was built against only the default skin and broke on the others — the colours adapted via tokens but the visual *devices* (glow ring, gradient) did not, because themes are design languages, not palettes, and nothing governs whether a theme does glow. Proposes a host theme contract: always-present semantic role tokens (incl. the missing on-accent + focus-ring), intent-named capability recipe slots where "off" is legal (an EMPHASIS recipe + an ACCENT-TEXT recipe), a window.feedBack.theme read/capability API + theme:changed event, a derive-surfaces-from-host reconciliation rule, accessibility baked in, and a skin-matrix verification gate. All additive + feature-detected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * docs(host-theme-contract): make the proposal normative (review fixes) Address the review of #645 (manual + Codex) — the doc was a sound design sketch but not yet the precise *contract* it claims to be: - P1 Token contract pinned: one public namespace `--fb-*` written on :root by a host-owned contract stylesheet (present themed-or-not); `--fbv-*` explicitly demoted to internal Tailwind-override plumbing (plugins must not read it). Added a normative role table + value grammar (colour roles = `r g b` triplets consumed via `rgb(var(--fb-x))`; recipe slots = full CSS device values). The §5 example now uses `--fb-*` throughout (was bare `--accent`/`--emph-*`). - P1 Invisible-text bug removed from the spec: `--fb-acc-text-fill` is the one slot where `none` is ILLEGAL (always a real paint, defaulting to the solid accent); the example feature-detects `background-clip: text` and keeps a solid `color` base, so the accuracy number can never render transparent — honouring the DoD "a device stays legible when its slot resolves to none". - P1 capabilities() booleans removed: they contradicted "never branch on glowy?" and were too lossy for canvas. The JS API is now CSS/DOM-forbidden and exposes RESOLVED token values (`get().tokens`) for canvas/WebGL renderers only. - P1 Physical home decided: a static `theme-contract.css` (outside the prebuilt Tailwind artifact, so no tailwind-fresh CI churn) holds the :root `--fb-*` defaults + the single focus-visible + reduced-motion rules; existing v3.css focus/motion rules are a tracked reconciliation, not day-one magic. - P2 Full on-fill family (`--fb-on-accent/-good/-warn/-bad`) + good/warn/bad ↔ existing good/mid/low mapping; `theme:changed` lifecycle pinned (get() sync + valid pre-apply, event after commit + once on hydration, plugins read on mount); same-document-light-DOM scope + shadow/iframe bridge stated; prefersReducedMotion() named the single JS motion gate. Resolved open questions folded into the body; the two genuine ones (skins-as- host-themes, component-recipe bundles) remain. 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> |
||
|
|
8a2175aa1c
|
feat(onboarding): amp-sim opt-in choice + use_amp_sims setting (#631)
Second half of feedBack-desktop#46. The desktop app monitors through an
in-app amp-sim/tone chain that, once loaded, auto-restores every launch —
an idle high-gain amp on the input is a constant distorted buzz, and the
dry-only monitor mute can't silence it. This adds the "own-rig first"
opt-in so players using their own external amp/rig never get a processed
monitor in the first place.
Core changes:
- New `use_amp_sims` setting (default OFF / own-rig first): GET default,
POST boolean validation, and resettable key — mirroring achievements_enabled.
- Onboarding wizard: a DESKTOP-ONLY step ("How do you want to hear
yourself?") between instrument paths and the calibration challenge. The
web build has no native amp sims, so the step is skipped there (5 steps
on web, 6 on desktop) — gated on window.feedBackDesktop, dot count and
setStep bounds are derived from it. Ticking "Use in-app amp simulations"
POSTs use_amp_sims; default unticked.
The desktop renderer consumes this setting to gate its saved-tone-chain
restore (feedback-desktop PR, paired).
Verified by booting core locally and walking the wizard with Playwright:
web shows 5 dots/no amp step, desktop shows 6 dots, the amp step is
reachable, calibration stays the final "Play it now" step, ticking the box
persists use_amp_sims=true, and there are no page errors. Server-side
GET default / POST validation / reset confirmed via curl.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
5a0b62599d
|
feat(highway): show feedpak author/editor credits on song load (#629)
Surface the feedpak manifest `authors` list (spec §5.4) on the highway: a credits card ("Charted by Azure") shown over the highway when a song loads, riding the count-in / a ~3s hold and dismissed when playback starts. Gated to fresh feedpak plays only (minigames, loose/archive, arrangement switches, seeks, replays excluded). Includes a 12s backstop so the overlay never lingers if playback fails to start.
Closes #628. Reviewed by Codex (3 passes, converged). Verified locally: pytest 9/9, node --test 23/23, headless-browser end-to-end.
|
||
|
|
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> |
||
|
|
290783b80b
|
feat(highway_3d): hit-feedback juice + Hit-sparks toggle (#618)
* feat(highway_3d): hit-feedback juice — cinematic lighting, strike line, sparks, intensity dial Charrette wave 1 (additive, default-tasteful, all behind settings): - #8 Hit-feedback settings: hitFx (0..1), cinematic, verdictMarks, timingFx, streakFx in BG_DEFAULTS + h3dBgSet* setters + settings.html (intensity slider + cinematic toggle). hitFx=0 → colour verdict only. - #2 Cinematic lighting: ambient 0.85→0.35 + stronger key light when cinematic on, so emissive gems have a dark surround to pop against. Live-toggleable. - #1 Strike line: a glowing bar at the hit line (Z=0) that flashes green on a verified hit / red on a miss, eased from the per-frame verdict alpha. - #3 Hit sparks: a pooled additive Points burst at the gem on a verified hit (deduped one burst per note), scaled by hitFx; disposed on teardown. Staged for wave 2 (after dogfooding): bloom+ACES (#4), colorblind verdict glyphs (#6), early/late timing tint (#5), streak heat + clean-bar (#7), gem scale-punch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Q9BpGYqUaga9ZJyS3dDPq * feat(highway_3d): wave 2 — gem scale-punch, streak heat, colorblind verdict marks - #3 (completion) gem scale-punch: the hit gem briefly grows (1 + 0.22·hitFx·alpha), biggest at the strike and easing with the verdict — the per-gem impulse. - #7 streak heat: a renderer-side consecutive-hit counter eases a 0..1 "heat" (plateau at 16) that grows the spark burst + warms the strike-line idle glow; a miss eases it back down. Behind the Streak-feedback toggle. - #6 colorblind verdict marks: a redundant ✓ (hit) / ✗ (miss) glyph on the verdict via the existing 2D label overlay, so the green/red pair isn't the only signal — notably also covers the provider path (where the timing labels don't show). - settings.html: Streak-feedback + Accessible-marks toggles. Deferred: #4 bloom+ACES (needs the Three.js postprocessing addons vendored into core static/vendor/three/ — not present; warrants its own infra change), and #5's timing tint (the early/late ±ms labels already render on the event path; surfacing them on the provider path needs a notedetect verdict field — a cross-plugin item). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Q9BpGYqUaga9ZJyS3dDPq * feat(highway_3d): #4 bloom + ACES — vendored Three.js postprocessing, perf-gated The single biggest fidelity lever from the charrette. Core had only three.module.min.js (no postprocessing addons), so this vendors the r170 EffectComposer/RenderPass/UnrealBloomPass/OutputPass + their shader deps into static/vendor/three/addons/, with every `from 'three'` rewritten to the SAME vendored three (../../three.module.min.js) so the addons share the plugin's three instance (a CDN copy would be a second, non-interoperable module). highway_3d wiring: - Lazy-loads the addons only when the new `bloom` setting is on (dynamic import), builds EffectComposer(RenderPass → UnrealBloomPass(strength .65/radius .5/ threshold .82 — high so only emissive gems + the hit flash bloom) → OutputPass). - Render loop uses composer.render() with ACES tone-mapping when bloom is active, else the unchanged direct ren.render() with NoToneMapping (bloom-off = today's look). - Perf-gated: OFF in splitscreen; graceful fallback to direct render if the modules or composer fail; composer.setSize on canvas resize; disposed on teardown. - settings.html: "Glow bloom" toggle (default on). Verified the import chain resolves + renders via a same-origin module-load test (EffectComposer built + a bloom frame rendered, three r170). Charrette status: 7/8 (only #5's early/late timing tint remains — a notedetect verdict-field change, outside the highway). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Q9BpGYqUaga9ZJyS3dDPq * feat(highway_3d): #5 early/late timing — colour the hit feedback by timing Surfaces the detector's timing on every hit (the charrette's last item), fully highway-side: notedetect already dispatches the judgment (timingState/timingError) on notedetect:hit/miss, so we carry timingState onto the event mark and tint the hit's spark burst + the ✓ verdict glyph by it — on-time green, early cyan, late amber. Gracefully falls back to green when no timing is known (pure-provider path), so it never invents data. Behind the new "Timing feedback" toggle (default on). Charrette: 8/8 complete. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Q9BpGYqUaga9ZJyS3dDPq * feat(highway_3d): add a "Hit sparks" on/off toggle (note-hit particles) The on-hit spark burst (the particle effect that fires the instant note_detect confirms a hit) could previously only be removed by dragging Hit-feedback intensity to 0 — which also kills the strike-line flash and the scale-punch. Add a dedicated "Hit sparks" toggle (default on) under 3D Highway settings, in the hit-feedback group beside the intensity slider, that gates ONLY the spark particles; the strike flash and colour verdict are unaffected. Wired the same way as the sibling juice toggles: a `sparks` boolean in BG_DEFAULTS, in _BG_BOOL_KEYS, a window.h3dBgSetSparks setter, the per-instance _sparks state + settings re-read, and a guard on the _sparkBurst spawn. Reuses existing Tailwind utility classes, so assets/plugin.css is unchanged; plugin.json version bumped to 3.28.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(highway_3d): act on tester charrette — strike line, fog readability, AA Addresses the alpha-tester 3D-highway feedback thread via the design panel's recommendations: - Strike line (panel rec 1a): now a HIT-ONLY faint "now" line — flashes green on a confirmed hit, no red miss branch (misses already show at the gem: red wash + ✗). Moved off the bottom edge to the vertical CENTRE of the string field, which was the "incorrectly placed" complaint (it read as the board's lower border and fused with open-string gems on a miss). Added a "Strike line" on/off toggle (`strikeLine`, default on). - Horizon readability (#2): the note gems + their outlines are now fog-exempt (`material.fog = false` on mStr/mGlow/mStrHitOutline/mHitBright/mWhiteOutline/ mMissOutline), so upcoming notes punch through the distance fog and stay legible as they render in — the board, lane, sustains and scenery keep their atmospheric fog, so depth is preserved. - Cinematic lighting softened: cinematic ambient 0.35 -> 0.45 so the dark stage doesn't crush note/fret legibility. - Anti-aliasing under bloom (perf rec): give the bloom EffectComposer a multisampled (WebGL2 MSAA x4) HalfFloat render target. The default target had no `samples`, so bloom-on bypassed MSAA — the "too HD / jagged on Windows, fine on Mac" report (Mac only won via Retina supersampling). This is the highest-value, smallest fix for the jaggies. plugin.json -> 3.29.0. The renderScale quality-oscillation is core (static/highway.js) and will be a separate feedBack PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(highway_3d): remove the strike line; sparks-only hit feedback, subtler Second tester-charrette pass. The strike line (even hit-only/centred from the last pass) was still too distracting/confusing on a hit, so it's removed entirely — strings + fret markers already orient the player, and the hit is fully carried at the gem (bright outline + scale-punch + spark burst) with the timing-coloured ✓/✗ verdict as the knowledge-of-results channel. - Deleted the strike-line mesh, its per-frame update, the `strikeLine` setting (BG_DEFAULTS / _BG_BOOL_KEYS / setter / settings-load), the settings.html toggle, and the now-dead `_strikeLine`/`_ndHitFlash`/`_ndMissFlash` state + their verdict-block feeds. - Made the spark burst subtler now that it's the sole celebration: point size 1.7→1.0·K, opacity 0.95→0.8, burst count (7+13·hitFx)→(4+7·hitFx), radial speed (7+r·20)→(5+r·12)·K, life (0.40+r·0.28)→(0.30+r·0.16)s. - Toggles for Hit sparks and the ✓/✗ verdict marks already exist in settings (kept). Minimal hit-feedback set now: gem bright + subtle spark (celebration) + timing-coloured ✓/✗ (the KR) + ambient streak heat. plugin.json -> 3.30.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(highway_3d): hydrate hit-feedback settings controls from saved state The 7 new juice controls (Hit sparks, Cinematic, Streak, Verdict marks, Bloom, Timing, Hit-feedback intensity) were hard-coded to their default markup and never read back from localStorage when the settings panel reopened — so a saved non-default (e.g. Hit sparks off) showed as the default (checked) even though the renderer correctly honored it. The sibling controls in the same panel were already hydrated; this restores that pattern for the new ones. Reads h3d_bg_* directly; defaults mirror BG_DEFAULTS (all bools on, hitFx 0.70) and the _bgCoerceBool 'true'/'1' vs 'false'/'0' coercion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
b206633131
|
fix(v3): keep Section Map's leftmost section clickable under the rail catcher (#617)
The section_map plugin pins a ~20px clickable bar (#section-map, z-index:5) to the top of #player. The v3 left-rail hover-catcher (.v3-railzone::before) is full-height at z-index:30 with pointer-events:auto, so its top-left corner swallowed every click on the section map's first section — the left-most section was never clickable on the v3 desktop (macOS/Windows) UI. Drop the catcher below the 20px bar when the section map is present, mirroring the existing #section-map ~ #player-hud special-case in static/style.css. The rail still reveals from anywhere below the bar. Adds a Playwright regression test (hit-test of the top-left corner) with a negative control that re-raises the catcher to reproduce the bug. Fixes #616 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a57d0e3f85
|
fix(v3): replace broken window.prompt() in Playlists with in-app uiPrompt modal (#614)
window.prompt() is a silent no-op in the Electron desktop shell, so the Playlists "New Playlist" and "Rename" buttons and the library's bulk "add selected songs to a playlist" action did nothing. Route all three through the existing window.uiPrompt() modal (resolves to the string, or null on cancel; the handlers were already async). window.confirm() works in Electron and is left as-is. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4480ac2732
|
feat(v3): promote Audio Engine to a first-class sidebar entry (after Settings) (#613)
The desktop Audio Engine plugin (input device selection, VST hosting, pitch detection, and the new config Reset/repair UI) was reachable only via the generic Plugins gallery — per-plugin manifest nav entries aren't surfaced in the v3 sidebar unless the plugin is promoted. Add it to PROMOTED_PLUGINS anchored after Settings, plus the matching NAV registry entry so the slot resolves its label/screen. Desktop-only by construction: the slot is filled only when /api/plugins reports audio_engine installed, so the web app shows no dead entry. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
6dbcc5861b
|
fix(player): keep play/pause button in sync when a JUCE reroute aborts autoplay's play() (#611)
On the first song after a fresh load on desktop, the audio engine is often still starting when the song loads, so the song begins on the HTML5 <audio> element and the engine-reroute watcher then migrates it to the JUCE backing transport. The reroute's first step is a deliberate audio.pause(), which rejects autoplay's in-flight togglePlay() audio.play() with an AbortError — even though playback continues on JUCE. togglePlay()'s catch then reset isPlaying=false and the button to "Play" while the song kept playing: the button showed Play during playback, so it took two clicks to actually pause (one to resync the flag, one to pause). The reroute already guards the <audio> 'play'/'pause' DOM listeners with window._juceRerouteInProgress; this extends the same guard to togglePlay()'s catch and the count-in catch, so a play() rejection caused by the reroute's own pause doesn't clobber the button. A genuine failure (outside a reroute) still resets correctly. Adds a regression test that drives togglePlay() through a reroute-aborted play() and asserts the button stays Pause; it fails without the guard. Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
8f0625e1f7
|
fix(v3): reset live performance HUD on backward seek / restart (#607)
The v3 live performance HUD (the visible top-right score tracker) keeps
its own hits/misses/streak counters from note:hit / note:miss events and
only reset them on song load / stop / ended — not on a seek. So pressing
Restart (or scrubbing back), which only repositions the playhead and
emits song:seek, left the tracker showing the stale cumulative score
(tester report).
Mirror the notedetect HUD fix: keep a per-note {t,hit} ledger (note:hit/
note:miss carry the judgment incl. noteTime) and, on a BACKWARD song:seek,
rebuild the tally to reflect only the notes up to the new playhead
(Restart -> "Waiting for notes" / 0). Forward seeks keep earlier notes;
loop-wrap (drill mode) is skipped so a practiced A-B loop still
accumulates, matching the notedetect HUD.
Tests: +3 in tests/js/live_performance_hud.test.js (backward rebuild,
restart-to-0, forward no-op, loop-wrap ignored). Existing 10 still pass.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
c8e0ad3f75
|
fix(gp-import): correct bass string count, lead/rhythm roles, preview note count (#601)
Four tester-reported GP-import issues, all in the converter/parse layer: * String count (bugs 2 & 4): <tuning> is padded to 6 slots, so a 4-string bass, 5-string bass and 6-string guitar were byte-identical and the real count was lost — a 5-string bass played on 4 strings and a 4-string bass showed a phantom B in the editor. Record the authoritative count in a new <tuning stringCount=N> attribute (gp2rs._build_xml) and trim the padded tail back to it on read (song.parse_arrangement). All consumers already trust a non-6 tuning length (arrangement_string_count, the editor's _stringCountFor and build-time _normalize_tuning_to_count), so this fixes the create-mode preview AND the built sloppak with no consumer changes. * Lead/Rhythm reversed (bug 3): guitar arrangements were named by appearance order (first guitar -> Lead), swapping roles for files that list Rhythm before Lead. Honor 'lead'/'rhythm' in the GP track name; unhinted tracks keep positional fallback. Applied to both convert_file's fallback (the editor's track_indices-without-names path) and _auto_select_gpx, with cross-role dedup so name-based and positional labels can't collide. * Preview note count (bug 1): the importer's per-track count included tie-continuation notes, which are folded into the previous note's sustain and never become separate RS notes (260 shown vs 241 imported). Exclude tie destinations so the preview matches the imported result. Adds regression tests for all three. Bug 5 (no stems from synced audio) is environment-dependent (best-effort demucs backend) and not addressed here. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
13bbfc0b3d
|
refactor(highway_3d): move Butterchurn controls into settings.html (#600)
Addresses the altitude finding from the Butterchurn review: the visualizer's on/off + slider options shipped as a parallel UI (a ~140-line floating in-canvas control panel) separate from the plugin's standard settings panel. Move the standard controls (Background on, opacity, dim-behind-lane + strength, chart accents + strength, color tint + strength, guitar gain, song gain) into settings.html, using the plugin's normal settings UI. They persist into the same 'viz3d_settings' blob the controller already reads; a new module-scope window.h3dBcApplySettings() hook lets settings.html push changes to a mounted highway live (it invalidates the controller's settings cache and re-applies). The in-canvas panel is now ONLY the live preset browser (pick / favorite / ban / cycle / hold / meters) — things that are inherently live tools and don't belong in a static settings form. cyclePool/hold and the favorites/bans lists stay there; reads were made cache-safe (read fresh via _bcLoadSettings) so a settings.html write can't be clobbered by a stale captured reference. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6fa5aabba8
|
fix(highway_3d): re-home Butterchurn panel to a surviving highway (splitscreen) (#599)
The Butterchurn control panel is a singleton, created only when a controller is created and parented to that controller's wrap. In splitscreen the panel followed the last-created controller; when that controller was torn down, destroy() only removed the panel DOM if it was the LAST controller, so with another highway still alive the panel stayed orphaned on the destroyed wrap and the surviving highway was left with no visualizer controls. Track each controller's wrap (ctrl.wrap) and, on destroy with another controller still alive, re-home the panel+pane onto the surviving primary's wrap via _bcEnsurePanel (which moves them when connected, or rebuilds them on the survivor if the old wrap was already detached). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3bb55ab854
|
feat(highway_3d): Butterchurn visualizer background style (#598)
* feat(highway_3d): add Butterchurn visualizer background style Adds an opt-in "Butterchurn (visualizer)" option to the 3D Highway plugin's Background-style dropdown. When selected, the highway renders over a WebGL MilkDrop (Butterchurn) canvas that reacts to your playing (guitar input on desktop, the song <audio> spectrum in the browser) and the chart (beat/note/ chord accents + instrument-color tint). The default stays 'particles', so existing users see no change until they pick it. Integrates the standalone "3D Highway + Butterchurn" mod into the bundled renderer as the 'butterchurn' bg-style (not a fork): - a self-contained _bc* controller that lazy-loads the vendored butterchurn libs only when the style is selected; mount/unmount is driven idempotently by the existing bg-style lifecycle (_bcSyncMode in _bgMountStyle) plus an explicit teardown in destroy() - the renderer uses alpha:true with the transparent clear gated on the mode, so every other bg style stays byte-identical (opaque clear) - the fog-scenery <audio> tap is disabled while active to avoid a double createMediaElementSource on #audio - the mod's slopsmith* globals are adapted to the current feedBack* names and the vendored asset URLs repointed to /api/plugins/highway_3d/assets/ Vendors butterchurn.min.js + butterchurnPresets.min.js (MIT) + viz-worklet.js under assets/vendor/; see plugins/highway_3d/NOTICE for attribution. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Q9BpGYqUaga9ZJyS3dDPq * fix(highway_3d): anchor Butterchurn panel to the highway, centered Addresses three issues found testing the visualizer control panel: - Attach the panel + preset pane to the 3D highway's `wrap` (position:absolute, pointer-events:auto) instead of position:fixed on document.body, so they sit on the highway's right edge and only exist while the highway is on-screen (no longer linger on the main menu / float at the app edge). - Re-home the singleton panel to the active highway wrap on mount, so it follows whichever highway is showing (e.g. moves off Virtuoso's embedded highway onto a normal song's highway) instead of sticking to the first one created. - Center it vertically (top:50% + translateY(-50%), folded into the slide transform) so a top overlay element no longer covers it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Q9BpGYqUaga9ZJyS3dDPq * fix(highway_3d): harden Butterchurn audio + lifecycle (review #597) Browser audio reactivity now REUSES the highway's existing shared analyser (the fog scenery's #audio / stems side-chain tap) instead of opening a second createMediaElementSource on #audio. The old _bcBrowserSource path: - threw InvalidStateError when the fog tap already owned #audio (default config), leaving the visualizer non-reactive in the browser, and could permanently disable fog reactivity if it tapped first (one-shot/element); - rerouted the song through a fresh, possibly-suspended AudioContext, which could MUTE playback when butterchurn was selected mid-song; - ignored the stems analyser, so it saw only silence on sloppak songs. _bcCreateController now takes an audioProvider (wired to _bgGetAnalyser) and connectAudio()s the shared AnalyserNode (a passthrough, so the fog's own reads are undisturbed). Also: - destroy() now closes the AudioContext when we own it (desktop / browser fallback), fixing a per-mount leak that hit the browser ~6-context cap after a few style toggles. The shared (fog-owned) context is never closed. - _bgApplyVenueSceneFog keeps the clear transparent while butterchurn is active, so the venue scene no longer occludes the visualizer. - _bcLoadLib no longer caches a rejected promise, so a transient vendor-load failure can be retried instead of disabling the feature for the session. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(highway_3d): Butterchurn lifecycle + audio re-bind (Codex preflight) Local Codex preflight on the Butterchurn feature flagged four issues; all fixed: - WebGL context leak on teardown: destroy() (and the async-init failure path) now call _bcReleaseCanvasGL() to force WEBGL_lose_context before dropping the canvas, so repeated mount/toggle cycles can't exhaust the browser's WebGL context cap. - Stale shared analyser across songs: the browser path captured the analyser once at mount, so a sloppak stems swap (new analyser, often new context) left the visualizer reacting to a dead node. update() now compares the live _bgGetAnalyser() against what the controller actually bound (boundAnalyser(), guarded by ready()) and either reconnects (same context) or rebuilds the controller (context changed) via the proven destroy()+_bcSyncMode paths. - Half-mounted controller on createVisualizer failure: the async .catch now cleans up (closes an owned AudioContext, removes layers, marks dead) and _bcSyncMode retries when bcCtrl.dead(), instead of leaking and never recovering. - _bcFfIdx off-by-one dropped accents landing exactly on a seek/loop target time; it now uses strict < so the update walkers fire the boundary event. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: ChrisBeWithYou <christian.a.cowan@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
97dae88860
|
feat(highway_3d): colour theming — string presets + Background/Highway scene themes (#596)
* feat(highway_3d): add one-click string-color presets
Adds 12 named string-color presets (Warm→Cool, Vivid, Colorblind-friendly,
Neon, Accessible, Warm Ember, Tape Deck, CRT Green/Amber, Pitch Ramp, Sunrise)
selectable from the 3D Highway settings panel.
Extends the existing core HWC (highway-color) subsystem in static/app.js with
HWC_PRESETS + applyHighwayStringPreset(), exposed on the existing facade as
window.feedBack.highwayColors.{presets, applyPreset}. The plugin settings page
renders the preset buttons from that core list and refreshes the per-string
pickers on apply. Purely additive — stock behavior is unchanged.
Scope: core static/app.js (the shared HWC facade both highways consume) plus the
highway_3d plugin's settings.html / screen.js / CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Q9BpGYqUaga9ZJyS3dDPq
* fix(highway_3d): address review of colour-theming PR
- Rebuild assets/plugin.css so the new `flex-wrap` (preset row) and
`text-[10px]` (theme-dropdown helper) Tailwind classes are actually
compiled, and bump plugin.json 3.26.0 -> 3.27.0 so the <link>'s ?v=
cache-buster fetches the fresh CSS (per the plugin's build rule).
- Replace the mirror-at-every-read hwTheme migration with a one-time
backfill (persist hwTheme := bgTheme on first load, no emit). The two
scene-color axes are now genuinely independent: changing the Background
dropdown no longer silently retints the Highway surface/lane, and the
rendered highway can't disagree with the Highway dropdown value.
- Collapse the duplicated theme id-set in settings.html (two identical
<option> lists + VALID_BG_THEMES) into a single SCENE_THEMES source the
dropdowns and validator are generated from; sync points 4 -> 2.
- Update CLAUDE.md to document the backfill + reduced sync contract.
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>
|
||
|
|
b70fde9b02
|
fix(player): new song no longer seeks to previous song's stop position (#595)
audio.currentTime does not reset synchronously when audio.src is cleared — it only resets when audio.load() is called (later, in highway.js). The jump-fix guard (setInterval ~line 8979) held lastAudioTime at the old position and, once the new song started playing from t=0, saw a 30s+ jump and sought the new song to the previous position. If the new song was shorter, song:ended fired immediately, showing the score screen. Reset lastAudioTime = 0 in playSong() so the guard has no stale anchor. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
4c3ec2ff66
|
feat(plugins): full-screen (immersive) plugin screens via manifest opt-in (#590)
DAW-style plugin UIs (e.g. a practice studio) need the whole viewport, not a scrolling content page below the v3 topbar — embedded in the shell they get cut off at the bottom with excess padding up top. Add an opt-in top-level `"fullscreen": true` plugin.json field, surfaced as the `fullscreen` boolean on /api/plugins (mirrors the settings_category plumbing in plugins/__init__.py). When a fullscreen plugin's screen is active, static/v3/ shell.js toggles `html.fb-immersive` from syncActive() so it tracks every navigation incl. deep-link; static/v3/v3.css then hides the topbar, collapses the sidebar to a functional icon rail (kept reachable — Escape is bound only on player/settings scopes, so a fully hidden sidebar would trap the user), and lets the active plugin screen fill #v3-main. Mirrors the existing ss-follower-pre chrome-hide pattern. Additive + opt-in: plugins without the flag are unaffected. Test: tests/test_plugins.py::test_fullscreen_flag_parsed_from_manifest Claude-Session: https://claude.ai/code/session_01BmWopMsRjdZyD6RwmZAQBv Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> |
||
|
|
8b4c9b0050
|
Fix list/tree view: select mode, parts visibility, song actions (#585)
* Fix list/tree view: select mode, parts visibility, song actions Bring the v3 list/tree view to parity with the grid card: - Select mode now renders a per-row checkbox + selected-ring, preserves expanded artist groups across re-render, and a capture-phase guard makes a row/chip click select the song instead of starting playback. - Always-on favourite / save-for-later / overflow-menu cluster on each row, same actions as the grid card. Rebuild static/tailwind.min.css so the new utilities are compiled in - notably .sm:flex behind the arrangement chips' "hidden sm:flex" wrapper. Without it the chips (and #582's badges) render display:none on the Docker build, which serves the committed CSS; the desktop build looked fine only because it rebuilds Tailwind from source at bundle time. Signed-off-by: Sin <deathlysin@outlook.com> * fix(v3): regenerate tailwind.min.css from source + add tree select tests + CHANGELOG The committed tailwind.min.css was over-built: 135,578 bytes / 1,428 selectors, with 294 selectors (accent-amber-400, bg-cyan-500, animate-spin, after:bg-gray-400, …) used in zero core source files — bloat from a local build scanning outside the repo's content globs. It would fail CI's rebuild-and-diff and violates the byte-stable rule in scripts/build-tailwind.sh. Regenerate via `scripts/build-tailwind.sh` (pinned tailwindcss@3.4.19): 111,491 bytes / 1,134 selectors, byte-identical to a clean rebuild, still containing the .sm\:flex fix plus every new tree class (ring-fb-primary, accent-fb-primary, pointer-events-none, …). Docker chips now render and CI stays green. Add tests/browser/v3-tree-select.spec.ts: - select mode keeps expanded artist groups open across the tree re-render (fails without loadTree's openArtists capture/restore) - clicking a row in select mode selects instead of playing Record the fix under CHANGELOG [Unreleased] -> Fixed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Sin <deathlysin@outlook.com> Co-authored-by: byrongamatos <xasiklas@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
82db8e56b1
|
chore(hotkeys): remove sloppak-convert library hotkey (#594)
* chore(hotkeys): remove sloppak-convert library hotkey
Removes the 'c' keyboard shortcut for converting library entries to
.sloppak. The shortcut was defined in two places:
- The no-op registerShortcut() entry that only existed to show in the
? help panel (the Sloppak Converter plugin handles conversion and
can register its own shortcut via window.registerShortcut).
- The c dispatch in the library-entry keydown handler
({ c: 'button.sloppak-convert-btn', ... }) that triggered the
plugin button.
* test+docs: update tests & CHANGELOG for removed `c` convert hotkey
The previous commit removed the `c` library hotkey but left three
assertions in tests/browser/keyboard-shortcuts.spec.ts that require it,
which fail deterministically (the two registry tests read window._panels
directly, independent of environment):
- should list all registered shortcuts (required {key:'c',scope:'library'})
- should have correct shortcut scopes (expected library::c)
- should show library shortcuts in help modal (Convert library entry / c)
Drop those assertions and record the removal under CHANGELOG
[Unreleased] -> Removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: byrongamatos <xasiklas@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
64801d5735
|
fix(player): Space bar play/pause when focus is on sidebar or rail buttons (#593)
* fix(player): Space bar play/pause when focus is on sidebar or rail buttons When any <button> in the player rail (viz, audio, mixer, etc.), a sidebar nav link, or a popover control has keyboard focus, pressing Space was blocked by _shortcutDispatchBlocked → _isInsideInteractiveControl, which returns true for BUTTON elements. The Space shortcut never reached the shortcut dispatcher and togglePlay() was never called. The fix extends the same carve-out pattern already used for the section practice bar: when the player screen is active, Space is always dispatched through the shortcut system. The shortcut handler's preventDefault() stops the focused element from also activating, so this is not a double-trigger. * test(player): cover Space play/pause carve-out + add CHANGELOG entry Adds two Playwright regression tests for #593 in tests/browser/keyboard-shortcuts.spec.ts: - Space toggles play/pause when a player rail <button> has focus, and the focused button does NOT also activate (dispatcher preventDefault). Fails on base (Space blocked, played=0), passes with the carve-out. - Space in a player-screen text input still types a space and never reaches play/pause (locks the _isTextInput exemption ordering). Also records the fix under CHANGELOG [Unreleased] -> Fixed, per the project workflow that every PR updates the changelog. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(player): don't override Space inside modal dialogs over the player The player-screen Space carve-out keyed off the active *screen*, so it also hijacked Space inside a true modal dialog layered over the player (e.g. the keyboard-shortcuts help modal, edit modal): Space toggled playback behind the modal and preventDefault blocked the modal's focused control (Close) from activating — contradicting aria-modal semantics. Narrow the carve-out to skip focus inside a modal (role="dialog" aria-modal="true" or .feedBack-modal). Non-modal player popovers/toasts (loop A/B, arrangement pin, role=dialog aria-modal=false) are not dialogs and stay covered, so the original fix is unchanged for the cases it targeted. Adds a Playwright regression test (Space inside a modal reaches the modal's button, not play/pause) and updates the CHANGELOG entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: byrongamatos <xasiklas@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d2569cc2a8
|
feat(achievements): wall sync drain worker + review fixes (epic PR3) (#592)
* feat(achievements): wall sync drain worker (epic PR3, client side) Background dead-letter worker that POSTs queued Feat unlocks/removals to the hosted feedback-achievements wall. Idle unless FEEDBACK_ACHIEVEMENTS_WALL_URL is set; uses requests + the client-token header (mirrors lyrics_transcribe). Dead-letter, never drop (pure engine.drain_decision): network err / 429 / 5xx -> keep pending (retry) other 4xx -> dead_letter (diagnosable, replayable) 2xx -> delete on server ack remove-me enqueues a wall removal keyed by the reused player_hash. Verified by an end-to-end staging round-trip (earn a Feat -> drains onto the wall with name + short hash -> remove-me -> wall empties) with no IP in tables or access logs. 42 plugin tests pass (test_sync.py adds the decision table + ack/retry/dead-letter retention + four-field on-the-wire payload). The hosted service lives in the new feedback-achievements repo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(achievements): address local review findings (epic) Bugs caught in the pre-merge review loop: - secret_witching Feat was DEAD: post_activity wrote witching_nights_run to the DB before snapshotting prev_tiers, so diff_unlocks never saw the fresh unlock. Fold the run into the activity delta instead (same asymmetry chart_encore uses) so the 7th-night unlock is detected. +regression tests. - chart_encore broke across restarts: per-chart counter keyed on abs(hash(str)), which Python salts per-process (PYTHONHASHSEED). Use a stable sha1 digest so the same chart accumulates across sessions. +regression test. - Bounded the per-activity counter read: _read_counters no longer pulls the unbounded chart_plays:* rows (they're bumped/read individually). - screen.js: gate note:hit/miss on an active-song flag so tuner/calibration note events can't inflate Feats or flush a phantom chart:null session. - screen.js: P-III — prefix the plugin localStorage key (achievements:profile-cat). - screen.js: extract the duplicated local-ISO-date helper. 45 plugin tests pass (3 new). Wall-side review fixes are in the feedback-achievements repo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(achievements): default the drain worker to the hosted wall Point FEEDBACK_ACHIEVEMENTS_WALL_URL's default at the live got-feedback wall (https://feedback-achievements.onrender.com) so the drain worker targets it out of the box; still env-overridable for self-hosting/staging. Nothing publishes unless the user opted in AND has a profile identity, so a default URL alone sends nothing. Tests disable the default (autouse fixture) so no test ever POSTs to production; drain logic is covered via _drain_once() with an injected poster. 45 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
287c23a532
|
feat(achievements): opt-in, privacy controls & data-min gate (epic PR2) (#591)
Sharing earned Feats on the (forthcoming) public wall is strictly opt-in,
default OFF, with a binding data-minimization contract.
- Onboarding (static/v3/profile.js): a new opt-in step (now a 5-step wizard)
after song-directory / before paths — publishes only display name + earned
Feats, never songs/skills/scores; off by default.
- Settings (plugins/achievements/settings.html, System tab via
settings.category): the same toggle + a "Remove me from the wall" button
(POST remove-me — wipes local synced state offline + enqueues removal).
- Core (server.py): achievements_enabled (bool, default false) in
_default_settings + /api/settings validation + _RESETTABLE_SETTINGS_KEYS;
mirrored to localStorage in app.js loadSettings().
- Data-minimization gate: engine.build_wall_payload is the single explicit-dict
serializer; key-set is EXACTLY {display_name, player_hash, achievement_id,
unlocked_at}, achievement_id always a Feat id. Enqueue is gated on
opted-in AND profile identity (reused player_hash); competency never
enqueues (integration law).
Verified natively: settings round-trip + validation + remove-me; opted-in
activity enqueues exactly one 4-field Feat payload; Playwright confirms the
5-step wizard + opt-in card (default unchecked), zero console errors.
29 plugin tests + new settings tests pass.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
05dd3d227a
|
feat(achievements): local engine + tabbed Profile shell (epic PR1) (#587)
Adds the Achievements & Feats of Power local engine, fully offline. Core (static/v3/profile.js): the Profile screen becomes tabbed exactly like v3 Settings (.fb-tabbar/.fb-tab/.fb-tabpanel, active tab persisted in localStorage 'v3-profile-tab'). A Profile (main) tab carries the existing cards + a Feats trophy-shelf mount (#v3-profile-feats-slot, earned-only), and an Achievements tab carries a plugin mount (#v3-profile-achievements-mount) + empty-state note. A new `v3:profile-rendered` event fires after every render so the plugin re-injects (mirrors v3:settings-rendered). New bundled plugin (plugins/achievements/): SQLite engine (unlocks/counters/comp_ledger/sync_queue) with pure threshold/criterion math in the testable sibling engine.py (P-V); routes activity/ report-unlock/report-criterion/catalog/earned/feats/remove-me. Feats read activity counters only (batched song:ended POST; notes only when notedetect present — graceful degradation); competency Achievements evaluate from progression events only — the integration law, never crossed. Catalogue is always shown (locked=greyed), grouped by the real progression paths (Global/Guitar/Bass/Drums/Keys, auto-extending) with per-category earned badges. Versioned window.feedBack.achievements registration API with the __feedBackAchievementsPending load-order queue + achievements:ready event. Verified natively (uvicorn) end-to-end + Playwright (tabbar, earned-only Feats shelf, greyed catalogue, registration API, zero console errors); 24 plugin tests pass incl. the integration-law assertion. Opt-in/privacy/data-min gate (PR2) and the hosted wall (PR3) follow. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
873ee3d5f2
|
fix(onboarding): rename diagnostic sloppak to feedBack- so "Play it now" finds it (#586)
The slopsmith→feedBack rename updated the diagnostic constant in server.py
(_BUILTIN_DIAGNOSTIC_SOURCES), the build script, README, and the calibration
test to `feedBack-diagnostic-basic-guitar.sloppak`, but the committed data
file was never regenerated/renamed — it stayed `slopsmith-diagnostic-...`.
Result: _seed_builtin_diagnostic_sloppaks() finds no matching source, silently
skips seeding, and the onboarding "Play it now" button (profile.js step 4 →
window.playSong) loads a file that isn't in the library. The server replies
{"error":"File not found"} and highway.js surfaces it as a native
`Error: File not found` popup. Affects all platforms.
Pure file rename to match the (already-renamed) code; no logic change.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|