This commit is contained in:
2026-02-12 05:01:03 -08:00
parent 502ffc0460
commit 62eff3427b

View File

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