[object Object]

← 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 →