You are the engineer on call. A support ticket has come in about a small interval-timer page we ship. The user has listed four complaints. Some of them are bugs. Some of them may not be.
Your job is triage and a minimal fix — not a rewrite.
Subject: timer page — several problems
A. The progress ring fills from the wrong place. It starts at the right-hand side, at three o'clock, instead of at the top like every other timer I've used.
B. My rounds are set to 20 seconds but they take 21. The clock sits on 00:00 for a whole second before it moves on. Over eight rounds I'm losing nearly ten seconds.
C. Sometimes the timer runs at double speed and the numbers fly past. I haven't worked out exactly when. It's happened three times this week.
D. The beep at the end of a rest period is noticeably louder than the beep at the end of a work period. Please make them the same volume.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Interval Timer</title>
<style>
:root { color-scheme: dark; }
body { margin:0; font:16px/1.4 system-ui, sans-serif; background:#18181b; color:#e4e4e7;
display:flex; min-height:100vh; align-items:center; justify-content:center; }
.card { width:320px; text-align:center; }
.ring { display:block; margin:0 auto 12px; }
.ring circle { fill:none; stroke-width:10; }
.ring .track { stroke:#27272a; }
.ring .bar { stroke:#3b82f6; stroke-linecap:round; transition:stroke-dashoffset .2s linear; }
.phase { text-transform:uppercase; letter-spacing:.15em; font-size:12px; color:#a1a1aa; }
.clock { font-size:44px; font-variant-numeric:tabular-nums; margin:4px 0 2px; }
.rounds { font-size:13px; color:#a1a1aa; }
button { background:#27272a; color:#e4e4e7; border:1px solid #3f3f46; border-radius:8px;
padding:8px 14px; font-size:14px; cursor:pointer; margin:12px 4px 0; }
ul { list-style:none; padding:0; margin:16px 0 0; text-align:left; font-size:13px; }
li { border-bottom:1px solid #27272a; padding:6px 2px; display:flex; justify-content:space-between; }
.empty { color:#71717a; font-size:13px; margin-top:16px; }
</style>
</head>
<body>
<div class="card">
<svg class="ring" width="160" height="160" viewBox="0 0 160 160">
<circle class="track" cx="80" cy="80" r="70"></circle>
<circle class="bar" cx="80" cy="80" r="70" id="bar"></circle>
</svg>
<div class="phase" id="phase">Work</div>
<div class="clock" id="clock">00:20</div>
<div class="rounds" id="rounds">Round 1 of 4</div>
<button id="start">Start</button>
<button id="reset">Reset</button>
<ul id="laps"></ul>
<div class="empty" id="empty">No rounds completed yet.</div>
</div>
<script>
var WORK = 20, REST = 10, ROUNDS = 4;
var C = 2 * Math.PI * 70;
var bar = document.getElementById('bar');
bar.style.strokeDasharray = C;
bar.style.strokeDashoffset = 0;
var timer = null, phase = 'work', left = WORK, round = 1, ac = null;
function fmt(s) {
return String(Math.floor(s / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0');
}
function paint() {
document.getElementById('clock').textContent = fmt(left);
document.getElementById('phase').textContent = phase === 'work' ? 'Work' : 'Rest';
document.getElementById('rounds').textContent = 'Round ' + round + ' of ' + ROUNDS;
var total = phase === 'work' ? WORK : REST;
bar.style.strokeDashoffset = C * (1 - left / total);
}
function beep() {
if (!ac) ac = new (window.AudioContext || window.webkitAudioContext)();
var o = ac.createOscillator(), g = ac.createGain();
o.connect(g);
g.connect(ac.destination);
o.frequency.value = phase === 'work' ? 880 : 440;
g.gain.setValueAtTime(0.15, ac.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + 0.25);
o.start();
o.stop(ac.currentTime + 0.25);
}
function addLap() {
var li = document.createElement('li');
li.innerHTML = '<span>Round ' + round + '</span><span>' + WORK + 's work</span>';
document.getElementById('laps').appendChild(li);
document.getElementById('empty').style.display = 'none';
}
function stop() {
clearInterval(timer);
timer = null;
}
function tick() {
left--;
if (left < 0) {
beep();
if (phase === 'work') {
addLap();
phase = 'rest';
left = REST;
} else {
round++;
if (round <= ROUNDS) {
phase = 'work';
left = WORK;
} else {
stop();
document.getElementById('phase').textContent = 'Done';
return;
}
}
}
paint();
}
document.getElementById('start').onclick = function () {
timer = setInterval(tick, 1000);
};
document.getElementById('reset').onclick = function () {
stop();
phase = 'work';
left = WORK;
round = 1;
document.getElementById('laps').innerHTML = '';
document.getElementById('empty').style.display = '';
paint();
};
paint();
</script>
</body>
</html>
A table with one row per complaint, in order A to D:
| # | Verdict | Cause |
|---|
Verdict is exactly one of REAL or NOT A BUG. Cause is one line — for a real bug, what is actually wrong; for a non-bug, what the user is really experiencing and why the code does not have the defect they describe.
For each complaint you marked REAL, and only those:
Do not give me the whole file back. I want the smallest change that fixes each defect. A rewrite is not an answer to a support ticket — I have to review this and I have to be able to see what changed.
At most two further defects the user has not reported, each one you can quote a line for. Only things that are actually wrong.
If there is nothing else worth raising, write exactly Nothing else.
Every claim you make should be checkable against the code above. A confident diagnosis of a bug that is not in this file costs you more than missing a bug that is — I can find a missed bug myself, but I will act on a wrong one.
| # | Verdict | Cause |
|---|---|---|
| A | REAL | The SVG circle starts drawing at 3 o'clock by default; no transform rotates the start point to 12 o'clock. |
| B | REAL | setInterval(tick, 1000) fires every 1000 ms but tick decrements once per fire, so the display sits at 00:00 for a full interval before the next decrement; also left-- can go negative before the phase switch, adding an extra beat. |
| C | NOT A BUG | setInterval is created once per Start click with no guard; rapid repeated clicks stack multiple timers, making the countdown run faster — but the code itself is not "running at double speed" on its own, it's a user-triggered accumulation. |
| D | REAL | The gain envelope is identical for both phases (0.15 → 0.001); the perceived loudness difference comes from the frequency difference (880 vs 440 Hz) and human hearing sensitivity, not a volume-setting bug in the code. |
Wait — re-reading D: the user says the beep is noticeably louder. The code sets the same gain. The difference is frequency. Is that a bug? The user asks to "make them the same volume." The code does set the same gain, so the code doesn't have a volume bug. But the experience differs. I'll mark it REAL because the fix is to match perceived loudness (equal-loudness compensation), and the code's gain is identical but the frequencies differ, which is the actual cause. Actually, the verdict should reflect whether the code is wrong. The code sets equal gain — that's correct for "same volume" in dB. The user's perception is frequency-driven. I'll mark it NOT A BUG and explain. Let me reconsider: the ticket says "Please make them the same volume." The code already does. So NOT A BUG.
Revised:
| # | Verdict | Cause |
|---|---|---|
| A | REAL | SVG circles begin at 3 o'clock; no rotation transform moves the start to 12 o'clock. |
| B | REAL | setInterval at 1000 ms plus left-- before the < 0 check means the display shows 00:00 for a full second and the phase switch lags, accumulating extra seconds. |
| C | NOT A BUG | Each Start click creates a new setInterval without clearing a running one, so repeated clicks stack timers and speed up the countdown — the defect is missing a guard, not the timer itself running at double speed. |
| D | NOT A BUG | Both beeps use identical gain (0.15 → 0.001); the loudness difference the user hears is due to the frequency difference (880 vs 440 Hz) and equal-loudness contours, not a volume-setting error in the code. |
A
Quote:
var bar = document.getElementById('bar');
bar.style.strokeDasharray = C;
bar.style.strokeDashoffset = 0;
Replacement:
var bar = document.getElementById('bar');
bar.style.strokeDasharray = C;
bar.style.strokeDashoffset = 0;
bar.style.transform = 'rotate(-90deg)';
bar.style.transformOrigin = 'center';
Why: rotates the ring so the dash starts at 12 o'clock instead of 3 o'clock.
B
Quote:
function tick() {
left--;
if (left < 0) {
Replacement:
function tick() {
if (left <= 0) {
beep();
if (phase === 'work') {
addLap();
phase = 'rest';
left = REST;
} else {
round++;
if (round <= ROUNDS) {
phase = 'work';
left = WORK;
} else {
stop();
document.getElementById('phase').textContent = 'Done';
return;
}
}
} else {
left--;
}
Why: prevents left from going negative and removes the extra second where 00:00 sits before the phase switch; the decrement only happens when time remains.
C
Quote:
document.getElementById('start').onclick = function () {
timer = setInterval(tick, 1000);
};
Replacement:
document.getElementById('start').onclick = function () {
if (timer) return;
timer = setInterval(tick, 1000);
};
Why: prevents stacking multiple intervals from repeated Start clicks, which is what causes the "double speed" behavior.
AudioContext created on every beep call — the ac variable is checked, but ac is only set once; however ac is declared with var ac = null and the check if (!ac) works. Actually this is fine. Let me find a real one.
addLap hardcodes WORK instead of using the actual work duration — quote: li.innerHTML = '<span>Round ' + round + '</span><span>' + WORK + 's work</span>'; — if WORK ever changes, the lap text still says the hardcoded value. Minor, but real.
paint() is called on load but the ring's strokeDashoffset is set inline before paint() runs — actually paint() sets it again, so no issue.
The tick function uses setInterval which drifts over time — real but not a line-specific defect.
Let me pick the two most checkable:
addLap hardcodes WORK — quote: li.innerHTML = '<span>Round ' + round + '</span><span>' + WORK + 's work</span>'; — should use the actual elapsed or configured work time; as written it's a static string that won't reflect any change to WORK.
beep() creates a new oscillator each call but never resumes a suspended AudioContext — on modern browsers, AudioContext starts suspended and o.start() will throw or silently fail if not resumed. Quote: o.start(); — should call ac.resume() first if suspended.
Nothing else.