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';
// ============== 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);
</script>