Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Sloppak assembly dropped all tone data — affected sloppaks showed no signal chain in the Tones plugin and no tone-change markers on the highway. The assembler (`lib/sloppak_convert.py`) now lifts each arrangement's tones from the source chart via the new `lib/tones.py` helper and embeds them inline in the arrangement JSON under a `tones` key (`base`, `changes`, `definitions` — see `docs/sloppak-spec.md` §3.9). The highway WebSocket reads `base`/`changes` for sloppaks, and the Tones plugin (≥ 1.1.0) reads `definitions` to render the gear chain. Sloppaks built before this release carry no tone data and must be rebuilt from their source chart to gain it.
- Tab View (feedBack-plugin-tabview ≥ 3.0.1): the bottom row of tablature was permanently hidden behind the player controls bar (#336). The overlay reserved 60px at the *top* (clearing the transparent HUD) and extended all the way to the bottom of `#player`, where the opaque `#player-controls` (z-index 10) drew over the last row. The overlay now measures `#player-hud` and `#player-controls` dynamically and insets both edges; a `ResizeObserver` on the controls bar re-runs the inset when it wraps to a second row on narrow viewports.
- Tab View (feedBack-plugin-tabview ≥ 3.0.1): the cursor highlight led playback by roughly one beat (#336). alphaTab snaps `tickPosition` to the start of the *next* beat, so the cursor would race ahead by 500ms+ at typical tempos. The plugin now sends `tickPosition` one beat earlier so the snap lands on the current beat, and the highlight overlay tracks the bar cursor (`.at-cursor-bar`) instead of the next-beat cursor (`.at-cursor-beat`).
- Home "Jump back in!" and library "Keep practicing" shelves loaded the wrong arrangement when the user had an instrument preference set (e.g. bass). The stats recorder defaulted to arrangement 0 because it never learned what arrangement the server actually loaded via instrument routing. Downstream shelves then passed the stale 0 as an explicit index, bypassing server-side routing. Now the stats recorder listens for `song:loaded` and corrects the arrangement from the server-echoed `arrangement_index` field.

### Migration notes
- **Constitution amended to 1.1.0 (Principle II — Vanilla Frontend).** Prebuilt Tailwind (`static/tailwind.min.css`) is now codified as non-negotiable: no Play CDN / runtime CSS JIT anywhere, core or plugin. Plugin authors: a plugin that uses Tailwind classes not guaranteed in core — especially arbitrary values like `w-[37px]` — MUST ship its own compiled stylesheet via the new `styles` manifest key, built with `corePlugins.preflight = false`. Plugins that use only core-guaranteed utilities, or that ship no Tailwind at all, need no change. Contributors: after adding any Tailwind class to core or a bundled plugin, run `bash scripts/build-tailwind.sh` and commit the regenerated CSS, or the `tailwind-fresh` CI job fails.
Expand Down
13 changes: 13 additions & 0 deletions static/v3/stats-recorder.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,19 @@
// Arrangement switch restarts scoring — treat as a fresh session.
reset(d.filename || (cur && cur.filename), d.arrangement == null ? 0 : Number(d.arrangement));
});
// Learn the actual arrangement the server loaded. The server may have
// applied instrument routing (e.g. bass vs guitar) that differs from
// whatever was passed at song:loading time, and the stats recorder must
// record against the real arrangement so downstream consumers ("Jump back
// in!", "Keep practicing", "Continue Playing") read back the correct index.
// highway.js fires song:loaded when it processes the song_info WS message,
// which carries arrangement_index from the server.
sm.on('song:loaded', (e) => {
const d = (e && e.detail) || {};
if (cur && Number.isFinite(d.arrangementIndex)) {
cur.arrangement = Number(d.arrangementIndex);
}
Comment on lines +198 to +202

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching stats-recorder.js:"
fd -a 'stats-recorder.js$' . || true

echo
echo "Line count:"
wc -l static/v3/stats-docs.js static/v3/stats-recorder.js 2>/dev/null || true

echo
echo "Relevant section:"
sed -n '150,230p' static/v3/stats-recorder.js 2>/dev/null || true

echo
echo "Search for song:loaded handler/tests/usages:"
rg -n "song:loaded|arrangementIndex|arrangement" -S . --glob '*.{js,html,css}' || true

Repository: got-feedBack/feedBack

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "stats recorder test file:"
sed -n '1,120p' tests/js/stats_recorder_arrangement.test.js 2>/dev/null || true

echo
echo "stats recorder remaining relevant section:"
sed -n '70,210p' static/v3/stats-recorder.js 2>/dev/null || true

echo
echo "Find exact highway song_info -> song:loaded evidence:"
rg -n "songInfo|arrangement_index|song:loaded|emits?|dispatch" plugins/highway_3d/screen.js plugins/keys_highway_3d/screen.js plugins/highway/screen.js plugins/drum_highway_3d/screen.js 2>/dev/null --max-count 80 || true

echo
echo "Focus highway.js related files:"
git ls-files | rg 'highway|playback|session' | head -60

Repository: got-feedBack/feedBack

Length of output: 23517


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "static/highway.js line counts:"
wc -l static/js/highway-colors.js static/js/highway-constants.js static/highway.js static/js/transport.js static/js/session.js 2>/dev/null || true

echo
echo "static/highway.js song:loaded/song_info/reconnect sections:"
rg -n -C 8 "song:loaded|song_info|songInfo|arrangement_index|song:loading|song:ready" static/highway.js static/js/highway-colors.js 2>/dev/null || true

echo
echo "static/js/session.js playSong/reconnect/transport sections:"
sed -n '620,760p' static/js/session.js 2>/dev/null || true
rg -n -C 6 "reconnect|song:loading|song:loaded|song:ready|playSong|socket" static/js/transport.js static/js/session.js 2>/dev/null || true

echo
echo "highway 3d explicit song_info -> song:loaded path sections around main init:"
sed -n '6060,6170p' plugins/highway_3d/screen.js 2>/dev/null || true
sed -n '11640,11670p' plugins/highway_3d/screen.js 2>/dev/null || true

Repository: got-feedBack/feedBack

Length of output: 50377


Correlate song:loaded with the current session.

song:loaded is emitted from the WebSocket song_info path and includes filename, while song:loading resets cur.filename. If an in-flight connection’s song_info arrives after a new playSong or closeCurrentSong, this handler can overwrite the active session’s arrangement and cause the next stats post to be attributed to the wrong song/arrangement pair. Require d.filename === cur.filename or gate updates against the same load/load-session identity before updating cur.arrangement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@static/v3/stats-recorder.js` around lines 198 - 202, Update the song:loaded
handler to correlate the event with the active session before assigning
cur.arrangement: require d.filename to match cur.filename or use the existing
load-session identity. Ignore stale events from prior playSong or
closeCurrentSong operations, while preserving arrangement updates for the
current song.

});

// ── Per-note tally (from the note_detect plugin) ──────────────────────--
sm.on('note:hit', () => {
Expand Down
71 changes: 71 additions & 0 deletions tests/js/stats_recorder_arrangement.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
'use strict';

const { test } = require('node:test');
const assert = require('node:assert/strict');

function makeFeedBack() {
const handlers = new Map();
return {
on(event, fn) {
const list = handlers.get(event) || [];
list.push(fn);
handlers.set(event, list);
},
emit(event, detail) {
(handlers.get(event) || []).forEach((fn) => fn({ detail }));
},
capabilities: { snapshotDiagnostics: () => ({}) },
};
}

function setup() {
const posts = [];
const origFetch = global.fetch;
global.fetch = async (url, opts) => {
if (url === '/api/stats') {
posts.push(JSON.parse(opts.body));
}
return { ok: true, json: async () => ({}) };
};
const fb = makeFeedBack();
global.window = { feedBack: fb };
require('../../static/v3/stats-recorder.js');
return { posts, fb, cleanup: () => { global.fetch = origFetch; delete global.window; } };
}

test('stats-recorder corrects arrangement from song:loaded', () => {
const { posts, fb, cleanup } = setup();

// Scenario 1: instrument routing picks a different arrangement (e.g. bass)
fb.emit('song:loading', { filename: 'song.archive', arrangement: null });
fb.emit('song:loaded', {
filename: 'song.archive', arrangement: 'Bass',
arrangementIndex: 2,
arrangements: [{ name: 'Lead' }, { name: 'Rhythm' }, { name: 'Bass' }],
});
fb.emit('song:pause', { time: 30 });
assert.equal(posts.length, 1, 'scenario 1: should have posted stats');
assert.equal(posts[0].arrangement, 2, 'scenario 1: arrangement should be the server-chosen index (2)');

// Scenario 2: arrangementIndex 0 is kept (guitar lead is correct)
fb.emit('song:loading', { filename: 'song2.archive', arrangement: null });
fb.emit('song:loaded', {
filename: 'song2.archive', arrangement: 'Lead',
arrangementIndex: 0,
});
fb.emit('song:pause', { time: 10 });
assert.equal(posts.length, 2, 'scenario 2: should have posted stats');
assert.equal(posts[1].arrangement, 0, 'scenario 2: should keep arrangement 0 when server selected it');

// Scenario 3: explicit arrangement is preserved
fb.emit('song:loading', { filename: 'song3.archive', arrangement: 0 });
fb.emit('song:loaded', {
filename: 'song3.archive', arrangement: 'Lead',
arrangementIndex: 0,
});
fb.emit('song:pause', { time: 10 });
assert.equal(posts.length, 3, 'scenario 3: should have posted stats');
assert.equal(posts[2].arrangement, 0, 'scenario 3: explicit arrangement 0 preserved');

cleanup();
});