diff --git a/sketches/doorbell/doorbell.html b/sketches/doorbell/doorbell.html index 0d45330..671927c 100644 --- a/sketches/doorbell/doorbell.html +++ b/sketches/doorbell/doorbell.html @@ -278,28 +278,27 @@ const STATUS_WS_URL = 'wss://ntfy.sh/STATUS_klubhaus_topic/ws'; // ============== BACKOFF CONFIGURATION ============== - const ACTIVE_DURATION = 180000; // 3 minutes full speed after interaction - const BASE_INTERVAL = 5000; // 5s when active - const BACKOFF_INTERVALS = [ // Progressive backoff after idle - 10000, // 0-5 min idle: 10s - 30000, // 5-10 min idle: 30s - 60000, // 10-20 min idle: 60s - 120000, // 20-40 min idle: 2min - 300000 // 40+ min idle: 5min + const ACTIVE_DURATION = 180000; + const BASE_INTERVAL = 5000; + const BACKOFF_INTERVALS = [ + 10000, + 30000, + 60000, + 120000, + 300000 ]; // ============== CLIENT ID (persistent, anonymous) ============== function getOrCreateClientId() { let id = localStorage.getItem('klubhaus_client_id'); if (!id) { - // Fingerprint from stable browser characteristics (no PII) const seed = [ - navigator.userAgent.split('/')[0], // Just "Mozilla" or "Chrome" + navigator.userAgent.split('/')[0], screen.width, screen.height, navigator.hardwareConcurrency || 'unknown', new Date().getTimezoneOffset(), - Math.random().toString(36).substring(2, 8) // 6 random chars + Math.random().toString(36).substring(2, 8) ].join('|'); id = 'web-' + hashString(seed).substring(0, 12); @@ -325,7 +324,6 @@ if (saved) { try { const parsed = JSON.parse(saved); - // Restore with defaults for missing fields return { totalPosted: parsed.totalPosted || 0, totalConfirmed: parsed.totalConfirmed || 0, @@ -352,7 +350,6 @@ localStorage.setItem('klubhaus_metrics', JSON.stringify(metrics)); } - // Initialize metrics from storage const metrics = loadMetrics(); // ============== BACKOFF STATE ============== @@ -374,16 +371,16 @@ const idleTime = Date.now() - lastInteraction; if (idleTime < ACTIVE_DURATION) { - return BASE_INTERVAL; // Full speed for 3 min + return BASE_INTERVAL; } const idleMinutes = (idleTime - ACTIVE_DURATION) / 60000; - if (idleMinutes < 5) return BACKOFF_INTERVALS[0]; // 10s - if (idleMinutes < 10) return BACKOFF_INTERVALS[1]; // 30s - if (idleMinutes < 20) return BACKOFF_INTERVALS[2]; // 60s - if (idleMinutes < 40) return BACKOFF_INTERVALS[3]; // 2min - return BACKOFF_INTERVALS[4]; // 5min + if (idleMinutes < 5) return BACKOFF_INTERVALS[0]; + if (idleMinutes < 10) return BACKOFF_INTERVALS[1]; + if (idleMinutes < 20) return BACKOFF_INTERVALS[2]; + if (idleMinutes < 40) return BACKOFF_INTERVALS[3]; + return BACKOFF_INTERVALS[4]; } function updateBackoffDisplay() { @@ -402,7 +399,6 @@ } } - // Update display every 10 seconds setInterval(updateBackoffDisplay, 10000); // ============== WEBSOCKET WITH ADAPTIVE RECONNECT ============== @@ -441,8 +437,6 @@ const nextInterval = getCurrentInterval(); connEl.textContent = `WebSocket: reconnect in ${nextInterval/1000}s...`; console.log(`WebSocket closed, reconnecting in ${nextInterval}ms...`); - - // Adaptive reconnect based on idle time setTimeout(connectWebSocket, nextInterval); }; } @@ -464,7 +458,7 @@ } metrics.totalReceived++; - saveMetrics(); // Persist immediately + saveMetrics(); updateStatusDisplay(payload); updateMetricsDisplay(); @@ -501,7 +495,7 @@ // ============== ALERT SENDING ============== async function sendAlert(message) { - recordInteraction(); // Reset backoff + recordInteraction(); if (isProcessing) return; isProcessing = true; @@ -558,7 +552,7 @@ const check = setInterval(() => { if (!isProcessing || Date.now() > deadline) { clearInterval(check); - resolve(!isProcessing); // Resolved if finishConfirmation called + resolve(!isProcessing); } }, 100); }); @@ -569,7 +563,6 @@ while (Date.now() < deadline) { try { - // Short poll without WebSocket const response = await fetch('https://ntfy.sh/STATUS_klubhaus_topic/json', { signal: AbortSignal.timeout(3000) }); @@ -716,9 +709,30 @@ el.textContent = `${CLIENT_ID} | posted:${metrics.totalPosted} confirmed:${metrics.totalConfirmed} errors:${metrics.totalErrors} avg:${avgRtt}ms | session:${sessionHours}h`; } - // ============== AGGREGATE METRICS (with backoff-aware interval) ============== + // ============== AGGREGATE METRICS (deduplicated) ============== + + let lastMetricsHash = null; + let lastMetricsTimestamp = 0; + + function hashMetricsPayload(payload) { + const canonical = JSON.stringify({ + client: payload.client, + totalPosted: payload.metrics.totalPosted, + totalConfirmed: payload.metrics.totalConfirmed, + totalReceived: payload.metrics.totalReceived, + totalErrors: payload.metrics.totalErrors, + avgRoundTripMs: payload.metrics.avgRoundTripMs + }); + + let hash = 0x811c9dc5; + for (let i = 0; i < canonical.length; i++) { + hash ^= canonical.charCodeAt(i); + hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24); + } + return (hash >>> 0).toString(16); + } + function scheduleMetricsPublish() { - // Clear existing if (metricsIntervalId) clearInterval(metricsIntervalId); const interval = getCurrentInterval(); @@ -740,18 +754,28 @@ } }; - try { - await fetch(METRICS_POST_URL, { - method: 'POST', - body: JSON.stringify(payload), - headers: { 'Content-Type': 'application/json' } - }); - console.log('Aggregate metrics published'); - } catch (e) { - console.error('Metrics publish failed:', e.message); + const currentHash = hashMetricsPayload(payload); + const now = Date.now(); + + if (currentHash === lastMetricsHash && (now - lastMetricsTimestamp) < 300000) { + console.log('Metrics unchanged, skipping emission'); + } else { + try { + await fetch(METRICS_POST_URL, { + method: 'POST', + body: JSON.stringify(payload), + headers: { 'Content-Type': 'application/json' } + }); + console.log('Aggregate metrics published'); + + lastMetricsHash = currentHash; + lastMetricsTimestamp = now; + + } catch (e) { + console.error('Metrics publish failed:', e.message); + } } - // Reschedule with potentially new interval scheduleMetricsPublish(); }, interval); @@ -762,7 +786,6 @@ if (e.key === 'Enter') sendCustom(); }); - // Track any interaction document.addEventListener('click', recordInteraction); document.addEventListener('touchstart', recordInteraction); document.addEventListener('keydown', recordInteraction); @@ -772,7 +795,7 @@ updateMetricsDisplay(); updateBackoffDisplay(); - console.log('KLUBHAUS ALERT v4.1 initialized'); + console.log('KLUBHAUS ALERT v4.2 initialized'); console.log('Client ID:', CLIENT_ID); console.log('Loaded metrics:', metrics);