Commit Graph
10 Commits
Author SHA1 Message Date
ChrisBeWithYouandClaude Fable 5 ffc52f13ce Harden freqs_to_midis against NaN/Inf; badges read exact tuningMidis
Follow-ups to #829 (CodeRabbit's review nit + the consumer adoption the PR
body promised):

- freqs_to_midis: reject non-finite frequencies (NaN/Infinity) — a provider
  handing one through would otherwise raise inside int(round(...)) and 500
  GET /api/tunings. Tests cover nan/inf/-inf alongside the existing garbage
  cases.

- v3 instrument badge: TUNING_NOTE now prefers the exact integer midis the
  server serves (tuningMidis) over reconstructing the note from the lowest
  string's frequency via log2 against a hardcoded 440 — which can land a
  semitone off at non-440 reference pitches. Frequency path kept as the
  fallback for older cached responses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MS2YFb6UUSwJVV6CmEa25i
Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
2026-07-12 15:24:39 -05:00
4b4c156fce refactor(ui): defer every classic script, keep boot() on DOMContentLoaded (R3a) (#872)
Puts every external `<script>` in the v3 shell into the deferred queue, and
keeps each script's boot() firing at DOMContentLoaded exactly as it does today.
Behaviourally a no-op; it is what makes the ES-module flips safe.

WHY. `type="module"` defers execution to after HTML parse. Classic-`defer` and
module scripts share ONE "execute after parsing" list and run in DOCUMENT ORDER,
but a plain classic script runs DURING parse — ahead of all of them. So the
moment capabilities.js becomes a module while app.js is still plain, app.js runs
FIRST, and its 11 top-level `window.feedBack.on(...)` calls (app.js:6245-6722)
hit a bare `{}` — `_ensureFeedBackEventBus()` (capabilities.js:33), which
attaches .on/.emit/.off, would not have run yet. TypeError, app.js dies
mid-parse. Deferring everything now keeps document order == execution order
through the rest of the migration.

THE CATCH (Codex preflight caught this — a real ordering change). 22 scripts
guard their boot with `if (document.readyState === 'loading')`. A deferred
script runs at readyState 'interactive', so that test is FALSE and the else-branch
fires boot() immediately, at the script's position in document order — instead of
at DOMContentLoaded, after every script has evaluated.

That matters far more than one call site: a scan of the shell's scripts found
**43 forward references** where a script's boot() reads a global that a LATER
script defines (shell.js -> profile.js's window.v3Onboarding, songs.js ->
settings.js's window._confirmDialog, badges.js -> songs.js's
window.displayTuningName, ...). Every one of them resolves today only because
all boots happen at DOMContentLoaded. So the guards now treat 'interactive' as
not-ready (`!== 'complete'`), restoring that exactly.

Codex's specific finding (first-run onboarding silently skipped) did NOT
reproduce — shell.js's boot() awaits /api/profile, and that yield lets the
remaining deferred scripts run first. But the race it described is real, the
guard is silent when it fails (`&& window.v3Onboarding`), and the other 42
forward refs have no such await protecting them. Fixed at the root rather than
at the one site.

VERIFIED. A/B against origin/main on a fresh profile, 13 probes (onboarding
overlay, v3Onboarding/v3Songs/v3Profile/fbNotify/v3Badges/uiPrompt/showScreen,
bus, capabilities.version, createHighway, plugin scripts, mounted screens):
IDENTICAL, zero console/page errors on both. pytest 2396, node 1028/1028,
ESLint 0 errors, Codex 0.

New guard: test_every_external_script_defers_so_document_order_is_execution_order
fails if any external tag is plain classic — verified to fail on a single
reverted tag, so it actually bites.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 16:57:04 +02:00
612b1f2e0d feat(v3): choose handedness in the instrument selector + onboarding (give lefties a break) (#793)
ship-ci / ci (push) Waiting to run
* feat(v3): add a handedness (left-handed) choice to the instrument selector + onboarding

Left-handed players could already mirror the highway, but only via a buried
Settings toggle they had to find AFTER setup -- so a lefty went through the tour,
the tuner and calibration all right-handed first (community callout).

Add a "Handedness: Right / Left" row to the v3 instrument badge popover, alongside
Instrument / Strings / Tuning (all player-orientation choices). It writes the same
lefty preference -- highway.setLefty when a live highway exists (flips it
immediately + persists), else the 'lefty' localStorage key the highway reads on
init -- and keeps the Settings "Left-handed" checkbox in sync. The first-run
tour's "Choose your instrument" step, which runs before the tuner/audio-
calibration steps, now calls it out so lefties flip it up front.

Frontend-only, additive. Full core JS suite green (938). Tests:
tests/js/badges_handedness.test.js.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UR2Cr7GEu3yMY7SrfxH6c1

* docs: split the spliced Handedness/Colorblind CHANGELOG entries

A rebase pasted the Handedness bullet over the Colorblind preset entry's bold
lead, merging two unrelated Added entries into one run-on bullet. Restore them
as two separate bullets.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
2026-07-05 23:50:30 +02:00
a86abadb14 settings: add host instrument profiles (#753)
* settings: add host instrument profiles

Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>

* settings: add instrument pathway selection

Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>

* fix(settings): profile-aware saves/resets/switch, provider tunings, bass-5

Five regressions from the instrument-profiles rework:

1. save_settings canonicalized profiles on EVERY save -> empty/unrelated POST
   froze default profiles into config.json (broke
   test_empty_post_preserves_all_existing_keys). Gate on the save touching
   instrument settings; GET already virtualizes profiles.
2. pathway is profile-mirrored, so the Gameplay reset (flat-key delete) was a
   no-op. reset_settings now resets pathway inside the persisted profiles too.
3. Per-profile tuning validation rejected provider/custom tunings (tuner
   plugin, /api/tunings). _valid_tuning_for_key now accepts a name unknown to
   every built-in table while still rejecting a built-in misapplied to the
   wrong key.
4. First-migration overwrote an explicit active_instrument_profile with the
   legacy-inferred one, so a fresh-config switch to 'bass' was lost. Use
   setdefault so an explicit request wins.
5. Pre-existing test_instrument_fields_persist used bass-5 + 'Drop D' (a
   4-string tuning). Updated to the valid 'Drop A'.

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

* fix(settings): partial-merge instrument_profiles; clamp tuning on string-count switch

Two partial-update follow-ups:
- save_settings normalized a POSTed instrument_profiles by FILLING every omitted
  profile with defaults and replacing wholesale, so a one-profile update reset
  the others. Validate each PROVIDED profile individually and merge the partial
  over the persisted set inside the lock — /api/settings is partial-merge.
- the string-count picker posted only string_count, so the backend silently
  reset a now-invalid tuning to Standard while the UI kept the old value
  (settings/tuner desync). Clamp + post the valid tuning too, mirroring the
  instrument-switch path.

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

---------

Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 00:16:42 +02:00
727b8c8f24 feat(a11y): app-wide "Interface size" setting (Accessibility) (#664)
* feat(a11y): app-wide "Interface size" setting (Accessibility)

Adds a dedicated Accessibility -> Interface size control so users on large,
low-DPI displays can enlarge the app's menus, buttons and text (reported: eye
strain on a 32" 1440p panel with no OS scaling).

Mechanism: a host-owned scale capability (window.feedBack.scale) applies a
RELATIVE root font-size (a % of the user-agent base, never a px literal, so a
raised browser/OS base font is respected) and publishes an always-present
--fb-scale token. The rem-based v3 chrome scales together; the gameplay highway
canvas (device-pixel sized) is deliberately untouched, so playback resolution
and FPS are unchanged. Medium (100%) clears the override, so default rendering
is byte-identical to before -- zero blast radius.

- Settings -> new Accessibility tab: Small/Medium/Large/Extra-Large presets
  (0.90/1.00/1.15/1.30) + a fine-tune slider (to 150%).
- Applied pre-paint from an inline <head> script (mirrors the ss-follower
  pattern) so there is no flash-of-reflow on load.
- window.feedBack.scale read-API (get/set + scale:changed, fires once on load)
  so canvas/WebGL surfaces that cannot inherit rem can follow the size. Shape
  mirrors the working-tuning read-API; persists as a durable preference.
- Cosmetic px->rem sweep so text scales cleanly at the larger stops (2 v3.css
  font-sizes + 20 text-[Npx] utilities across 8 v3 files; tailwind.min.css
  rebuilt byte-stable via the pinned toolchain).
- One-time first-run nudge for the large/low-DPI display profile that deep-links
  to the control (never fires once the setting is touched, or on other displays).

v3-only. Verified headless: core apply/persist/reset/reload/UI-sync + nudge
gating, with no console errors.

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

* fix(v3): regenerate tailwind.min.css to satisfy tailwind-fresh CI (PR #664 review)

Regenerate static/tailwind.min.css via scripts/build-tailwind.sh
(tailwindcss@3.4.19) so a fresh build matches the committed artifact and
the tailwind-fresh CI job's `git diff --quiet` passes. Two consecutive
regenerations are byte-identical.

Also (Fix 2) switch the fine-tune interface-size slider to the documented
transient-preview path: oninput now calls scale.set(v, { persist:false })
so dragging previews without writing localStorage/emitting a commit each
tick, and a new onchange commits with persistence on release.

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>
2026-07-02 14:00:30 +02:00
7173007412 feat(v3): show the live working tuning on the instrument card (working-tuning PR 5) (#667)
* feat(v3): show the live working tuning on the instrument card (working-tuning PR 5)

The topbar instrument selector now surfaces the selected instrument's live
working tuning from the host workingTuning capability (PR 1): a compact label on
the card — dim while you're in your home tuning, amber once you've retuned — and,
in the dropdown, a "Now in: <tuning> <assumed/verified glyph>" banner with a
one-tap "Back to default" (resetToDefault). Switching instrument/strings calls
setCurrentInstrument so the card follows the right instrument (guitar's tuning vs
bass's, tracked separately); it re-renders on working-tuning-changed.

Names offsets via the shared window.displayTuningName resolver. Fully
feature-detected — without the workingTuning capability the card renders exactly
as before. v3-only, single file (static/v3/badges.js).

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

* fix(badges): consistent home-tuning check + gate working-context on save (PR #667 review)

Review fixes for the instrument-card working-tuning label:

- isHome used display-label equality where the home label was the raw settings
  string ('Custom') but the working label came from displayTuningName(offsets) —
  a real home tuning could show amber. Both now resolve through the same namer.
- The instrument pill moved the working-tuning context (setCurrentInstrument) even
  when saveSettings() rejected the patch, desyncing the selector from the card.
  saveSettings() now returns whether it was accepted; the pill only switches the
  working context on success.

(No boot-time setCurrentInstrument: the host already seeds its current instrument
from /api/settings and emits working-tuning-changed on hydration — which this card
re-renders on — so pre-touching would only race/suppress that seed.)

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>
2026-07-01 11:01:42 +02:00
6aed8510d7 Tuner: passive "different tuning" badge cue naming the retune (issue E, stage 2.5/3) (#657)
* Tuner: passive "different tuning" badge cue that names the retune

Building on the coverage check: when you enter a song your current
instrument doesn't cover, the topbar tuner badge gets an amber ring + a
tooltip naming the change (e.g. "retune B->A", or "the reference pitch"
for an A440 vs A432 mismatch). Advisory only -- it never auto-opens the
panel; recomputed on song:ready, cleared on song-load / leaving the
player.

Refactors the coverage check into a structured report
(window._tunerAutoOpen.coverageReport -> { covered, retune:[{from,to}],
reference, cantCover }); the boolean gate now wraps it. The cue is
CSS-free (inline ring + native tooltip, no Tailwind rebuild) and no-ops
when the tuner plugin is absent.

Touches static/v3/badges.js (cue) + plugins/tuner/screen.js (report).
v3-only. Stacked on #656 (issue E stage 2.5/3). The splitscreen-suppress
and no-usable-input guards move to E2 (the playback gate).

Tests: tests/js/tuner_auto_open.test.js (report names the strings,
reference mismatch, badge wiring).

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

* feat(tuner): read/write the live per-instrument working tuning — both-directions retune prompt (working-tuning PR 3) (#660)

The §4 coverage check compared each song against the player's fixed
instrument-profile tuning, so the tuner only ever prompted *away* from a "home"
tuning (E -> Drop C#) and stayed silent coming back (Drop C# -> E), even though
the player had physically retuned.

_playerTuning() now reads the host's live per-instrument working tuning
(window.feedBack.workingTuning, keyed by the selected instrument from
/api/settings) instead of re-deriving from the static settings tuning, so
coverage is measured against what the instrument is ACTUALLY in and prompts both
directions. On clearing an auto-opened tuner, _publishWorkingTuning() writes that
song's tuning as the instrument's live working tuning ('assumed' — PR 4's
explicit "I tuned / Skip" refines the write-point), so the next song is judged
against where the player now is.

Per-instrument (guitar vs bass tracked separately). Feature-detected: falls back
to the static /api/settings tuning when the working-tuning capability is absent,
so the 27 existing coverage tests are unchanged. Builds on PR 1 (host
workingTuning) + PR 2 (instrument->chart routing).

Tests: tests/js/tuner_auto_open.test.js — +2 (both-directions coverage via a live
Drop-D working tuning; publish-on-clear targets the right instrument slot); 29
pass total.

Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tuner): transactional open + fail-closed auto-open config (tuner-E #655 review) (#681)

Two review fixes for the auto-open opt-in+persist stage:

- enable() wasn't transactional. The panel (with the ×/Skip buttons) is shown
  before `await _tunerAudio.start()`, and `_state.enabled` was only set after it.
  A ×/Skip dismiss during that await hit disable() with wasEnabled=false, then
  enable() completed and flipped enabled on — an enabled-but-hidden zombie. Guard
  the open with an `_openGen` token bumped on every enable()/disable(); after the
  audio-start await, bail if superseded instead of enabling. Closes #675.

- Config wasn't fail-closed. routes.py normalized the opt-in with
  bool(data.get("autoOpenOnTuningChange", False)), so "false"/"0"/junk coerced to
  True. Accept only a real JSON boolean. Closes #676.

Tests: tuner_auto_open.test.js (dismiss-mid-open stays disabled — fails without
the token guard), test_config.py (auto-open default-false + fail-closed on
non-bool). 34 JS + 24 config tests green.

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

* fix(tuner): coverage stays conservative when the instrument is unknown (tuner-E #656 review) (#682)

_playerTuning() is documented as conservative ("missing data → not covered → still
prompt"), but when /api/settings carried no instrument identity (a fresh profile:
_default_settings() omits instrument/string_count/tuning) it invented guitar/6/440/
standard, so an unconfigured player was treated as 6-string E-standard and coverage
suppressed the auto-open (and badge cue) for matching songs. The post-#660 rewrite
only returned null when the whole fetch failed (!s), not when settings existed but
lacked an instrument.

Now return null unless there's a confident identity — any of instrument/string_count/
tuning in settings, or live working-tuning offsets. A configured standard guitar still
covers a standard song (no regression). Closes #677.

Tests: tuner_auto_open.test.js — empty-settings → not covered (fails without the fix);
configured standard guitar → still covered.

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

* fix(tuner): badge coverage cue staleness + unknown-as-warning + dedupe (tuner-E #657 review) (#683)

Three review fixes for the passive "different tuning" badge cue stage:

- Stale async cue (#678): _refreshCoverageCue awaited coverageReport then wrote the
  DOM unconditionally, so a slow /api/settings fetch could restore the previous
  song's amber ring after song:loading / leaving the player. Add a monotonic token
  bumped on every refresh and both clear paths; apply the awaited report only if the
  token still matches.

- "Unknown" rendered as "needs retune" (#679): the plugin returns a conservative
  all-false report on a fetch hiccup; the cue painted that as an amber "retune the
  reference pitch" ring. Collapse a no-signal report (not covered, no reference /
  retune / cantCover) to null (no cue) via _meaningfulReport(). A genuine not-covered
  report always carries reference / retune / cantCover, so real cues are preserved.

- Duplicate /api/settings fetch (#680): the auto-open gate and the badge cue both
  call coverageReport() per song:ready. Cache the coverage promise per song (keyed by
  session + tuning + centOffset) so they share one fetch; invalidate on song:loading,
  instrument:changed, and working-tuning-changed so it can't go stale within a song.

Tests: tuner_auto_open.test.js — concurrent reports share one fetch, a new song
refetches (fails without the cache). 34 JS tests green. Codex-reviewed.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Byron Gamatos <xasiklas@gmail.com>
2026-07-01 10:28:04 +02:00
af2949677a rename: slopsmith → feedBack, byron → got-feedBack (#537)
* Update GitHub repo references from feedback* to feedBack*

* rename: slopsmith -> feedBack, byron -> got-feedBack

Renames across the entire codebase:
- slopsmith/Slopsmith/SLOPSMITH/SlopSmith -> feedBack/FeedBack/FEEDBACK/FeedBack
- byron/Byron/Byrongamatos -> got-feedBack/got-feedBack/got-feedBack
- /home/byron/ -> /opt/got-feedBack/
- byron@ougsoft.com -> hi@got-feedBack.org
- github.com/byrongamatos/ -> github.com/got-feedback/
- com.byron. -> com.got-feedback.
- SLOPSMITH_ env vars -> FEEDBACK_ with backward-compat fallback
- Protocol/storage strings migrated with read-old/write-new pattern
- window.slopsmith JS API -> window.feedBack (canonical) + backward-compat alias

Refs: #rename-slopsmith

* rename: complete regen against current main + fix backward-compat alias

Regenerated the slopsmith->feedBack / byron->got-feedBack rename on top of
current main (3 commits had landed since the branch: #572/#554/#574),
resolving the four content conflicts in favour of main's newer content
(autoplay/auto-exit, accuracy-badge, Virtuoso re-home, feedpak badge).

Completion fixes on top of the mechanical rename:
- Re-apply rename to post-branch content the original rename never saw:
  window.slopsmith(.Tour) consumers in lessons.js / notifications.js /
  onboarding-tour.js, and the matching JS + python tests (autoplay_exit,
  progression_*, test_feedpak_extension FEEDBACK_* env vars). The test env
  vars now match server.py (which reads FEEDBACK_SYNC_STARTUP /
  FEEDBACK_SKIP_STARTUP_TASKS), so the sync-startup test exercises the real
  path again.
- Restore the window.slopsmith backward-compat alias dropped during conflict
  resolution, and move the bus aliases to AFTER the _feedBackExisting merge
  block so they reference the fully-assembled object (also fixes the
  loop_api.test.js API-surface regex, which the original PR latently broke).
- Drop the stray empty data/web_library.db (runtime DB lives in CONFIG_DIR)
  and gitignore it.
- Fix stale tone-source test: feed[dB]ack -> fee[dB]ack to match shipped
  source labels.

Verified locally (org CI billing-blocked): JS 819/819 pass; pytest 1669
passed / 1683 collected with 0 import errors; zero residual slopsmith/byron
except the two intentional window.slopsmith aliases.

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

* rename: implement advertised backward-compat + prune dead community plugins

Address gaps where PR #537's "Backward compatibility" section was advertised
but not implemented, and clean up the community plugin list.

Env vars (FEEDBACK_* canonical, legacy SLOPSMITH_* honoured):
- New lib/env_compat.py (getenv_compat / env_flag_compat) + tests. server.py
  (_env_flag + all FEEDBACK_* reads), diagnostics_hardware, gp2midi and
  tailwind_rebuild now resolve the legacy alias, so existing SLOPSMITH_UI /
  SLOPSMITH_PLUGINS_DIR / etc. deployments keep working.
- Fix the rename collapsing plugins/__init__.py and minigames/routes.py from
  `FEEDBACK_PLUGINS_DIR or SLOPSMITH_PLUGINS_DIR` into a redundant
  `FEEDBACK_ or FEEDBACK_` (the fallback was silently lost).

Storage (app.js update-channel):
- Read feedBack-update-channel, fall back to legacy slopsmith-update-channel,
  and clear the legacy key on write — so a user's update-channel preference
  survives the rename instead of resetting to "stable".

Community plugin list (README): the rename rewrote third-party repo URLs we
don't own. Probed every one; their owners never renamed, so:
- Restore the 13 live community plugins to their real slopsmith-* names.
- Prune 6 that are 404 to the public (topkoa splitscreen/stems, OmikronApex
  tuner, Jafz2001 nam-rig-builder, DeathlySin song-preview, Erikcb91 shuffle).
- Fix a pre-existing Guitar Theory clone-command typo (nam-tone -> guitar-theory).

Verified: env_compat 7/7, JS 819/819, pytest 1690 collected / 0 import errors,
rename-sensitive + startup suites green.

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

---------

Co-authored-by: byrongamatos <xasiklas@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:03:01 +02:00
c7fb074111 feat(onboarding): first-run home tour (spotlight coach marks) (#528)
* style(tour): align tour engine + Shepherd bubbles to the v3 fb-* palette

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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