snapshot
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user