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);