feedBack/static/js
Byron Gamatos 8d3db5f42c
Some checks are pending
ship-ci / ci (push) Waiting to run
fix(nav): nobody may monkey-patch window.showScreen — add screen:changing, make the shell listen (#924) (#925)
* fix(nav): the library sometimes showed the legacy screen — map 'home' inside showScreen

Testers: "randomly, when moving to the library from another menu option, the library shows the
old interface — never when a song ends."

━━━ WHAT WAS ACTUALLY HAPPENING ━━━

#home is the PRE-V3 library screen. The v3 shell replaced it with #v3-songs, and the mapping DID
exist — but only inside WRAPPERS on window.showScreen, and only for callers that go through
`window`. THREE independent parties monkey-patch it, each capturing whatever happens to be there
at the time:

    app.js publishes the raw function
      -> shell.js wraps it, adding the home -> v3-songs mapping
      -> the stems plugin wraps it AGAIN (src/main.js:1029), capturing the current value

Plugins load ASYNCHRONOUSLY. The chain links up in whatever order the race settles, and any
capture taken before shell.js installs — or any re-assignment after it — silently drops the
mapping. Hence "randomly".

AND THE INTERNAL CALLERS NEVER TOUCHED window.showScreen AT ALL. closeCurrentSong and the
Esc-from-settings shortcut call the IMPORTED showScreen, which no wrapper ever sees. Reproduced
in a browser: the unwrapped function with 'home' lands on the dead legacy screen EVERY time.

"Never when a song ends" is the tell, and it is what identified the mechanism: closeCurrentSong
resolves its target through _resolvePlayerOrigin(), which ALREADY applies this mapping. That one
path was fine — which is exactly why the bug looked random rather than total.

PRE-EXISTING, not a regression from the module carve: the onclick="showScreen('home')" links and
the wrapper-only mapping both date to 2026-06-22.

━━━ THE FIX ━━━

The guard lives inside showScreen now: ONE place, in the function every caller routes through,
instead of a chain of monkey-patches that must each remember. Wrapper order stops mattering, and
the module-internal callers are covered for the first time.

Verified in a browser: the raw, unwrapped showScreen('home') — which reproduced as #home — now
lands on #v3-songs, and cannot be undone by any wrapper order.

━━━ AND A [P1] I INTRODUCED, WHICH CODEX CAUGHT ━━━

My first cut mapped BOTH 'home' and 'v3-home', copied straight from _resolvePlayerOrigin.

That is correct THERE and wrong HERE. _resolvePlayerOrigin computes where to RETURN TO after a
song, and landing on the Songs list from the dashboard is the right behaviour. But #v3-home is
the v3 DASHBOARD — a real screen that the shell's Home nav, the onboarding tour and the dashboard
re-render listener all target. Redirecting it would have made Home unreachable.

A LEGACY ALIAS IS NOT THE SAME THING AS A RETURN TARGET. Only 'home' is mapped now, and a test
pins that: re-adding 'v3-home' to the guard fails it.

4 tests, bite-tested both ways.

node 1049, pytest 2425, ESLint 0, Codex 0.

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

* fix(nav): nobody may monkey-patch window.showScreen — add screen:changing, make the shell listen (#924)

window.showScreen was wrapped by THREE independent parties, each capturing whatever happened to be
there at the time:

    app.js publishes the raw function
      -> static/v3/shell.js wrapped it (to call syncActive, and to map home -> v3-songs)
      -> the stems plugin wrapped it AGAIN (to tear down on leaving the player)

Plugins load ASYNCHRONOUSLY, so the chain linked up in whatever order the race settled. A capture
taken before shell.js installed silently dropped the mapping it carried — and the library opened on
the dead legacy #home screen. Testers saw that as "randomly, the library shows the old interface"
(#923).

#923 fixed the symptom by moving the mapping inside showScreen. This removes the CAUSE: neither
wrapper ever needed to be one.

━━━ TWO EVENTS, AND THE DISTINCTION IS THE WHOLE POINT ━━━

    screen:changing  emitted BEFORE anything happens. "I am leaving `from`." Teardown/cancel here.
    screen:changed   emitted after the DOM and data settle. "I am on `id`." Now carries `from`.

screen:changing is new, and it exists because Codex caught me collapsing the two. The stems plugin
tore down its audio graph BEFORE showScreen did anything; screen:changed fires at the very END,
after core awaits library and provider loads — so moving the plugin onto it would have delayed
teardown behind a slow fetch, or skipped it entirely if that fetch threw, and stems would keep
playing on a non-player screen. A test pins the ordering: screen:changing must precede the first
await.

shell.js is a plain screen:changed listener now, like app.js, audio-mixer.js and tour-engine.js
already were. window.showScreen is an unwrapped function again, and tests/js/
no_showscreen_monkeypatch.test.js fails CI if anything in static/ ever assigns to it again — so the
hazard is structurally impossible rather than merely avoided.

━━━ AND A FALLBACK THAT COULD NEVER FIRE ━━━

My retry-if-the-bus-is-late path listened for `slopsmith:capabilities:ready`. Core dispatches
`feedBack:capabilities:ready` (capabilities.js:1536) — the slopsmith: name is the PRE-DMCA event
and nothing has emitted it since the rename. Codex caught it. A guard that cannot fire is worse
than no guard: it reads as protection and is decoration.

(The same dead-event bug turned out to be sitting in THREE of the stems plugin's fallbacks, where
it has silently disabled its lifecycle wiring whenever the bus was late. Fixed in
feedback-plugin-stems#38.)

VERIFIED. A/B against origin/main: the nav highlight and topbar title follow IDENTICALLY with
shell.js as a listener; screen:changing -> screen:changed fire in order with the right {id, from};
window.showScreen is unwrapped; and showScreen('home') still lands on v3-songs.

node 1053, pytest 2425, ESLint 0, Codex 0.

Closes #924

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-07-12 16:06:28 +02:00
..
audio-el.js refactor(app): give the <audio> element a module of its own (R3a) (#886) 2026-07-11 20:44:01 +02:00
count-in.js refactor(highway): make the highway global explicit before the module flip (R3c) (#912) 2026-07-12 11:56:07 +02:00
diagnostics-export.js refactor(app): carve the diagnostics-bundle export out of app.js (R3a) (#881) 2026-07-11 18:52:47 +02:00
dom.js refactor(app): carve the DOM/modal primitives out of app.js (R3a) (#882) 2026-07-11 18:59:44 +02:00
edit-modal.js refactor(app): carve the edit-song modal into static/js/edit-modal.js (R3d) (#919) 2026-07-12 13:43:57 +02:00
format.js refactor(app): give formatTime a home — a leaf format.js, one fewer host hook (R3a) (#895) 2026-07-11 23:27:17 +02:00
highway-colors.js refactor(highway): make the highway global explicit before the module flip (R3c) (#912) 2026-07-12 11:56:07 +02:00
highway-constants.js refactor(highway): carve the constants into static/js/highway-constants.js (R3c) (#914) 2026-07-12 12:24:40 +02:00
highway-draw.js refactor(highway): carve the 2D drawing layer into highway-draw.js (R3c) (#917) 2026-07-12 13:00:29 +02:00
highway-geometry.js refactor(highway): carve the 2D drawing layer into highway-draw.js (R3c) (#917) 2026-07-12 13:00:29 +02:00
highway-state-primitives.js refactor(highway): carve the STATEFUL primitives, threading hwState explicitly (R3c) (#916) 2026-07-12 12:42:38 +02:00
host.js refactor(app): the host seam + carve section practice out of app.js (R3a) (#887) 2026-07-11 20:58:41 +02:00
juce-audio.js refactor(highway): make the highway global explicit before the module flip (R3c) (#912) 2026-07-12 11:56:07 +02:00
library-state.js refactor(app): carve the library out of app.js (R3a) (#896) 2026-07-11 23:30:30 +02:00
library.js refactor(app): carve the library out of app.js (R3a) (#896) 2026-07-11 23:30:30 +02:00
loops.js refactor(app): give formatTime a home — a leaf format.js, one fewer host hook (R3a) (#895) 2026-07-11 23:27:17 +02:00
player-controls.js refactor(highway): make the highway global explicit before the module flip (R3c) (#912) 2026-07-12 11:56:07 +02:00
player-state.js refactor(app): carve the JUCE/desktop audio shims out of app.js (R3a) (#893) 2026-07-11 22:44:03 +02:00
plugin-loader.js fix(plugins): make a module plugin actually re-evaluate on reload (#879) (#897) 2026-07-12 00:19:59 +02:00
resume-session.js refactor(highway): make the highway global explicit before the module flip (R3c) (#912) 2026-07-12 11:56:07 +02:00
section-practice.js refactor(highway): make the highway global explicit before the module flip (R3c) (#912) 2026-07-12 11:56:07 +02:00
session.js fix(nav): nobody may monkey-patch window.showScreen — add screen:changing, make the shell listen (#924) (#925) 2026-07-12 16:06:28 +02:00
settings-io.js refactor(app): carve settings backup + plugin updates out of app.js (R3a) (#885) 2026-07-11 19:42:54 +02:00
settings.js refactor(app): carve settings into static/js/settings.js (R3d) (#920) 2026-07-12 14:08:45 +02:00
shortcuts.js refactor(app): carve the keyboard-shortcuts subsystem into static/js/shortcuts.js (R3d) (#922) 2026-07-12 16:05:25 +02:00
transport.js refactor(highway): make the highway global explicit before the module flip (R3c) (#912) 2026-07-12 11:56:07 +02:00
tuning-display.js refactor(app): carve the tuning-display helpers out of app.js (R3a) (#884) 2026-07-11 19:37:44 +02:00
viz.js refactor(highway): make the highway global explicit before the module flip (R3c) (#912) 2026-07-12 11:56:07 +02:00