diff --git a/index.html b/index.html
index 2f17c90..b088db8 100644
--- a/index.html
+++ b/index.html
@@ -199,6 +199,95 @@
const messageEl = document.getElementById('message');
const POLL_INTERVAL = 2000;
+ // Sound system
+ let audioCtx = null;
+ let soundEnabled = new URLSearchParams(window.location.search).get('sound') === 'on';
+ let lastState = null;
+
+ function initAudio() {
+ if (!audioCtx) {
+ audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+ }
+ if (audioCtx.state === 'suspended') {
+ audioCtx.resume();
+ }
+ }
+
+ function playTone(frequency, duration, type = 'sine', volume = 0.1) {
+ if (!soundEnabled || !audioCtx) return;
+ const osc = audioCtx.createOscillator();
+ const gain = audioCtx.createGain();
+ osc.type = type;
+ osc.frequency.value = frequency;
+ gain.gain.value = volume;
+ gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
+ osc.connect(gain);
+ gain.connect(audioCtx.destination);
+ osc.start();
+ osc.stop(audioCtx.currentTime + duration);
+ }
+
+ function playWarningSound() {
+ // Soft double-beep
+ playTone(440, 0.15);
+ setTimeout(() => playTone(440, 0.15), 180);
+ }
+
+ function playCriticalSound() {
+ // Urgent descending tone
+ playTone(600, 0.2);
+ setTimeout(() => playTone(400, 0.3), 220);
+ }
+
+ function playNotifySound() {
+ // Gentle ping
+ playTone(880, 0.1, 'sine', 0.08);
+ }
+
+ function playRecoverySound() {
+ // Happy ascending chirp
+ playTone(523, 0.1);
+ setTimeout(() => playTone(659, 0.1), 100);
+ setTimeout(() => playTone(784, 0.15), 200);
+ }
+
+ function handleStateChange(newState, newEmote) {
+ if (!lastState) {
+ lastState = newState;
+ return;
+ }
+
+ // State transitions that trigger sounds
+ if (newState !== lastState) {
+ if (newState === 'critical') {
+ playCriticalSound();
+ } else if (newState === 'warning') {
+ playWarningSound();
+ } else if (newState === 'notify') {
+ playNotifySound();
+ } else if (newState === 'optimal' && lastState !== 'optimal' && lastState !== 'sleeping') {
+ // Recovery - also check for celebration emote
+ playRecoverySound();
+ }
+ lastState = newState;
+ }
+ }
+
+ // Enable sound on first tap (browser autoplay policy)
+ document.body.addEventListener('click', () => {
+ if (!soundEnabled) {
+ soundEnabled = true;
+ initAudio();
+ // Brief confirmation chirp
+ playTone(660, 0.08, 'sine', 0.05);
+ }
+ }, { once: false });
+
+ // Also init if ?sound=on
+ if (soundEnabled) {
+ document.addEventListener('DOMContentLoaded', initAudio);
+ }
+
async function fetchStatus() {
try {
const response = await fetch('/status');
@@ -216,6 +305,9 @@
}
function updateDisplay(data) {
+ // Check for state changes and play sounds
+ handleStateChange(data.current_state, data.active_emote);
+
emoteEl.textContent = data.active_emote;
emoteEl.style.color = data.color;
messageEl.style.color = data.color;