webpage_generator: success chime on finish + trim mobile empty scroll space

- Audio: a normal finish (scanning -> idle) now plays a rising success
  arpeggio (C5-E5-G5) instead of the falling warning chime, so a completed
  measurement is audibly distinct from a stop to any other state (which keeps
  the falling 660->440 warning). startWarning(finished) selects the chime for
  both the initial tone and the 30s repeats; the Watch LED text reads
  "Measurement finished" vs "Measurement stopped" accordingly. Help text
  updated.
- Layout: body uses min-height:100dvh (with 100vh fallback) so the page no
  longer extends below the visible viewport on mobile, where iOS 100vh counts
  the retracted-toolbar height and leaves empty scroll room at the bottom.
This commit is contained in:
x12sa
2026-07-07 10:41:40 +02:00
committed by holler
co-authored by holler
parent 6222cafb62
commit d880431a5a
@@ -1633,7 +1633,11 @@ def _render_html(phone_numbers: list, has_tomo_queue: bool = True, tomo_types: d
body {{
background: var(--bg); color: var(--text);
font-family: var(--sans); font-weight: 300;
min-height: 100vh; padding: 1.5rem;
min-height: 100vh; /* fallback for browsers without dvh */
min-height: 100dvh; /* dynamic viewport: avoids extra scroll room
below content on mobile (iOS 100vh counts the
retracted-toolbar height and overshoots) */
padding: 1.5rem;
transition: background 0.4s, color 0.4s;
}}
body::before {{
@@ -2157,7 +2161,7 @@ def _render_html(phone_numbers: list, has_tomo_queue: bool = True, tomo_types: d
<h3>Audio warnings</h3>
<ul>
<li><strong>System LED</strong> &mdash; on when audio is enabled on this device.</li>
<li><strong>Watch LED</strong> &mdash; armed (green) while a scan is running; pulses orange if a scan stops unexpectedly, with a chime every 30s until confirmed.</li>
<li><strong>Watch LED</strong> &mdash; armed (green) while a scan is running; pulses orange when a scan stops, with a chime every 30s until confirmed. A normal finish (scan &rarr; idle) plays a rising success tone; a stop to any other state plays the falling warning tone, so you can tell them apart by ear.</li>
<li><strong>Blocked LED</strong> &mdash; pulses orange while the queue is locked. A chime fires automatically after 60 continuous minutes blocked, then repeats hourly until cleared. No confirmation needed &mdash; it clears itself once the block resolves.</li>
<li><strong>Live LED</strong> &mdash; green while this page's data feed is fresh; pulses orange if the feed goes stale or a fetch fails.</li>
</ul>
@@ -2353,6 +2357,7 @@ initDrag();
let audioCtx=null, audioEnabled=false;
let audioArmed=false, warningActive=false, warningTimer=null, lastStatus=null;
let warningWasSuccess=false; // true when the active warning is a normal finish (scanning -> idle)
let staleActive=false, staleTimer=null, staleConfirmed=false;
let blockedSince=null, blockedWarningActive=false, blockedChimeTimer=null;
const BLOCKED_WARNING_DELAY_MS=60*60*1000; // first chime after 60 min continuously blocked
@@ -2387,6 +2392,13 @@ function beep(freq,dur,vol){{
function warningChime(){{
beep(660,0.3,0.4); setTimeout(()=>beep(440,0.5,0.4),350);
}}
function successChime(){{
// Reverse of warningChime: a rising major arpeggio (C5-E5-G5) so a normal
// finish (scanning -> idle) is audibly a happy "done" rather than an alert.
beep(523,0.22,0.4);
setTimeout(()=>beep(659,0.22,0.4),240);
setTimeout(()=>beep(784,0.45,0.4),480);
}}
function staleChime(){{
beep(1200,0.12,0.35);
setTimeout(()=>beep(1200,0.12,0.35),180);
@@ -2432,12 +2444,14 @@ function confirmWarning(){{
updateAudioUI();
}}
function startWarning(){{
function startWarning(finished){{
// Not a gesture handler — context already unlocked by Enable button click.
if(warningActive) return;
warningActive=true;
if(audioEnabled) warningChime();
warningTimer=setInterval(()=>{{ if(audioEnabled) warningChime(); }},30000);
warningWasSuccess=!!finished; // scanning -> idle == successful finish
const chime = warningWasSuccess ? successChime : warningChime;
if(audioEnabled) chime();
warningTimer=setInterval(()=>{{ if(audioEnabled) chime(); }},30000);
document.getElementById('btn-confirm').style.display='inline-block';
updateAudioUI();
}}
@@ -2537,10 +2551,10 @@ function updateAudioUI(){{
txt.textContent='Audio disabled \u2014 enable to receive warnings';
}}else if(warningActive && staleActive){{
ledWatch.className='led led-warning';
txt.textContent='Measurement stopped & live feed lost \u2014 confirm each';
txt.textContent=(warningWasSuccess?'Measurement finished':'Measurement stopped')+' & live feed lost \u2014 confirm each';
}}else if(warningActive){{
ledWatch.className='led led-warning';
txt.textContent='Measurement stopped \u2014 confirm to silence';
txt.textContent=(warningWasSuccess?'Measurement finished':'Measurement stopped')+' \u2014 confirm to silence';
}}else if(staleActive){{
ledWatch.className='led';
txt.textContent='Live feed lost \u2014 confirm to silence';
@@ -2573,7 +2587,7 @@ function handleAudioForStatus(status, prevStatus){{
}}
if(audioArmed && wasScanning && !isScanning){{
startWarning();
startWarning(status==='idle'); // idle == finished successfully -> success chime
}}
if(isScanning && warningActive){{