← back to Scarlet Riverboat Masquerade
Harden music room per codex review: fetch race-token guard, per-track audio-error skip (ignore benign ABORTED), keyboard guard for form fields, drop role=application
6aa7d78248bafec24df6a117bec81176f77d041a · 2026-09-01 17:45:58 -0700 · Steve Abrams
Files touched
Diff
commit 6aa7d78248bafec24df6a117bec81176f77d041a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Sep 1 17:45:58 2026 -0700
Harden music room per codex review: fetch race-token guard, per-track audio-error skip (ignore benign ABORTED), keyboard guard for form fields, drop role=application
---
index.html | 26 +++++++++++++++++++++-----
1 file changed, 21 insertions(+), 5 deletions(-)
diff --git a/index.html b/index.html
index d5fe2fd..c849d94 100644
--- a/index.html
+++ b/index.html
@@ -196,8 +196,10 @@
<body>
<a href="#dock" class="skip">Skip to controls</a>
<a href="/" class="backlink" id="backlink" aria-label="Back to DEAD, the listening exhibition">◆ DEAD · A Listening Exhibition</a>
-<main id="stage" role="application" aria-label="Scarlet Riverboat Masquerade — an animated visual gallery you can steer with the controls and keyboard">
+<main id="stage" aria-label="Scarlet Riverboat Masquerade — an animated visual gallery you can steer with the controls and keyboard">
<h1 class="sr">Scarlet Riverboat Masquerade — an animated generative gallery</h1>
+ <!-- role removed from <main>: role="application" suppressed normal screen-reader navigation over the native controls + prose -->
+
<canvas id="sky" aria-hidden="true"></canvas>
<canvas id="scene" aria-hidden="true"></canvas>
@@ -1043,8 +1045,11 @@ function announce(msg){ live.textContent=msg; }
// keyboard shortcuts
window.addEventListener('keydown',e=>{
- // Let native controls handle their own Space/Enter activation
- if(e.target.matches('input,button')&&(e.key===' '||e.key==='Enter')) return;
+ const t=e.target;
+ // While a form field has focus, let it own every key (arrows in the show <select> must not switch chapters)
+ if(t && t.matches && t.matches('input,select,textarea')) return;
+ // Let native buttons handle their own Space/Enter activation
+ if(t && t.matches && t.matches('button,[role="button"]') && (e.key===' '||e.key==='Enter')) return;
switch(e.key.toLowerCase()){
case ' ': e.preventDefault(); playBtn.click(); break;
case 'r': document.getElementById('restartBtn').click(); break;
@@ -1083,7 +1088,7 @@ const DEFAULT_SHOW = 3; // Cornell '77
const $ = id => document.getElementById(id);
const audio = $('mrAudio'), room = $('musicroom'), sel = $('mrShow'), live = $('live');
-let showIdx=-1, tracks=[], trackIdx=-1, seeking=false;
+let showIdx=-1, tracks=[], trackIdx=-1, seeking=false, loadToken=0;
CATALOG.forEach((s,i)=>{ const o=document.createElement('option'); o.value=String(i); o.textContent=`${s.date} — ${s.title}`; sel.appendChild(o); });
@@ -1099,6 +1104,7 @@ function setFoot(s){
async function loadShow(i, opts){
opts=opts||{};
+ const myToken=++loadToken; // guard against a stale fetch overwriting a newer selection
try{ audio.pause(); }catch(e){} // stop the current night before loading the next
showIdx=i; tracks=[]; trackIdx=-1;
const s=CATALOG[i];
@@ -1111,6 +1117,7 @@ async function loadShow(i, opts){
const r=await fetch(`https://archive.org/metadata/${s.id}`,{mode:'cors'});
if(!r.ok) throw new Error('metadata '+r.status);
const j=await r.json();
+ if(myToken!==loadToken) return; // a newer show was selected while we awaited — drop this result
let mp3=(j.files||[]).filter(f=>/vbr mp3/i.test(f.format||''));
if(!mp3.length) mp3=(j.files||[]).filter(f=>/mp3/i.test(f.format||''));
mp3.sort((a,b)=> (a.name>b.name?1:-1));
@@ -1127,6 +1134,7 @@ async function loadShow(i, opts){
announce(`Loaded ${s.title}, ${s.date}. ${tracks.length} tracks from the Live Music Archive.`);
if(opts.play) playTrack(0);
}catch(e){
+ if(myToken!==loadToken) return; // superseded — let the newer selection own the UI
fallbackEmbed(s);
}
}
@@ -1176,7 +1184,15 @@ audio.addEventListener('timeupdate',()=>{
$('mrCur').textContent=fmt(c); $('mrDur').textContent=fmt(d);
if(d>0 && !seeking) $('mrSeek').value=String(Math.round(c/d*1000));
});
-audio.addEventListener('error',()=>{ if(showIdx>=0 && !tracks.length) fallbackEmbed(CATALOG[showIdx]); });
+audio.addEventListener('error',()=>{
+ const err=audio.error;
+ if(err && err.code===err.MEDIA_ERR_ABORTED) return; // fired by our own src swaps — not a real failure
+ if(showIdx<0) return;
+ if(!tracks.length){ fallbackEmbed(CATALOG[showIdx]); return; }
+ // a specific track wouldn't stream (404/geo/transient) — move on rather than stall silently
+ if(trackIdx>=0 && trackIdx<tracks.length-1){ announce('That track was unavailable; skipping ahead.'); playTrack(trackIdx+1); }
+ else { setPlay(false); const m=$('mrMsg'); m.hidden=false; m.textContent='That track could not be streamed from the archive right now — try another night.'; }
+});
$('mrPlay').addEventListener('click',()=>{
if(!tracks.length){ loadShow(showIdx<0?DEFAULT_SHOW:showIdx,{play:true}); return; }
← 2b38b3c Add conditional DEAD back-link (shown only when served as /r
·
back to Scarlet Riverboat Masquerade
·
Add optional 'React to Music' mode: Web Audio AnalyserNode o 3e917c7 →