v0.7.34 - We seemed to maybe finally fixed the monitoring error?

This commit is contained in:
Your Name
2025-10-22 10:19:43 -04:00
parent 9cb9b746d8
commit 9179d57cc9
21 changed files with 2877 additions and 503 deletions

View File

@@ -23,6 +23,7 @@ let currentConfig = null;
let relayPool = null;
let subscriptionId = null;
let isSubscribed = false; // Flag to prevent multiple simultaneous subscriptions
let isSubscribing = false; // Flag to prevent re-entry during subscription setup
// Relay connection state
let relayInfo = null;
let isRelayConnected = false;
@@ -307,6 +308,13 @@ async function restoreAuthenticationState(pubkey) {
// Automatically set up relay connection based on current page URL
async function setupAutomaticRelayConnection(showSections = false) {
console.log('=== SETUP AUTOMATIC RELAY CONNECTION CALLED ===');
console.log('Call stack:', new Error().stack);
console.log('showSections:', showSections);
console.log('Current isRelayConnected:', isRelayConnected);
console.log('Current relayPool:', relayPool ? 'EXISTS' : 'NULL');
console.log('Current isSubscribed:', isSubscribed);
try {
// Get the current page URL and convert to WebSocket URL
const currentUrl = window.location.href;
@@ -322,8 +330,9 @@ async function setupAutomaticRelayConnection(showSections = false) {
}
// Remove any path components to get just the base URL
// CRITICAL: Always add trailing slash for consistent URL format
const url = new URL(relayUrl);
relayUrl = `${url.protocol}//${url.host}`;
relayUrl = `${url.protocol}//${url.host}/`;
// Set the relay URL
relayConnectionUrl.value = relayUrl;
@@ -348,13 +357,8 @@ async function setupAutomaticRelayConnection(showSections = false) {
relayPubkey = '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa';
}
// Initialize relay pool for admin API communication
if (!relayPool) {
relayPool = new window.NostrTools.SimplePool();
console.log('🔌 Initialized SimplePool for admin API communication');
}
// Set up subscription to receive admin API responses
// Note: subscribeToConfiguration() will create the SimplePool internally
await subscribeToConfiguration();
console.log('📡 Subscription established for admin API responses');
@@ -615,16 +619,22 @@ async function loadUserProfile() {
'wss://relay.nostr.band',
'wss://nos.lol',
'wss://relay.primal.net',
'wss://relay.snort.social',
'wss://relay.laantungir.net'];
'wss://relay.snort.social'
];
// Get profile event (kind 0) for the user
const events = await profilePool.querySync(relays, {
// Get profile event (kind 0) for the user with timeout
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Profile query timeout')), 5000)
);
const queryPromise = profilePool.querySync(relays, {
kinds: [0],
authors: [userPubkey],
limit: 1
});
const events = await Promise.race([queryPromise, timeoutPromise]);
if (events.length > 0) {
console.log('Profile event found:', events[0]);
const profile = JSON.parse(events[0].content);
@@ -648,8 +658,14 @@ async function loadUserProfile() {
// Keep the npub display
}
// Close the profile pool
profilePool.close(relays);
// Properly close the profile pool with error handling
try {
await profilePool.close(relays);
// Give time for cleanup
await new Promise(resolve => setTimeout(resolve, 100));
} catch (closeError) {
console.log('Profile pool close error (non-critical):', closeError.message);
}
} catch (error) {
console.log('Profile loading failed: ' + error.message);
@@ -755,20 +771,25 @@ async function logout() {
// Stop auto-refresh before disconnecting
stopStatsAutoRefresh();
// Clean up configuration pool
// Clean up relay pool
if (relayPool) {
log('Closing configuration pool...', 'INFO');
log('Closing relay pool...', 'INFO');
const url = relayConnectionUrl.value.trim();
if (url) {
relayPool.close([url]);
try {
await relayPool.close([url]);
} catch (e) {
console.log('Pool close error (non-critical):', e.message);
}
}
relayPool = null;
subscriptionId = null;
// Reset subscription flag
isSubscribed = false;
}
// Reset subscription flags
isSubscribed = false;
isSubscribing = false;
await nlLite.logout();
userPubkey = null;
@@ -778,12 +799,9 @@ async function logout() {
// Reset relay connection state
isRelayConnected = false;
relayPubkey = null;
// Reset subscription flag
isSubscribed = false;
// Reset UI - hide profile and show login modal
hideProfileFromHeader();
// showLoginModal() removed - handled by handleLogoutEvent()
updateConfigStatus(false);
updateAdminSectionsVisibility();
@@ -815,42 +833,204 @@ function generateSubId() {
return result;
}
// WebSocket monitoring function to attach to SimplePool connections
function attachWebSocketMonitoring(relayPool, url) {
console.log('🔍 Attaching WebSocket monitoring to SimplePool...');
// SimplePool stores connections in _conn object
if (relayPool && relayPool._conn) {
// Monitor when connections are created
const originalGetConnection = relayPool._conn[url];
if (originalGetConnection) {
console.log('📡 Found existing connection for URL:', url);
// Try to access the WebSocket if it's available
const conn = relayPool._conn[url];
if (conn && conn.ws) {
attachWebSocketEventListeners(conn.ws, url);
}
}
// Override the connection getter to monitor new connections
const originalConn = relayPool._conn;
relayPool._conn = new Proxy(originalConn, {
get(target, prop) {
const conn = target[prop];
if (conn && conn.ws && !conn.ws._monitored) {
console.log('🔗 New WebSocket connection detected for:', prop);
attachWebSocketEventListeners(conn.ws, prop);
conn.ws._monitored = true;
}
return conn;
},
set(target, prop, value) {
if (value && value.ws && !value.ws._monitored) {
console.log('🔗 WebSocket connection being set for:', prop);
attachWebSocketEventListeners(value.ws, prop);
value.ws._monitored = true;
}
target[prop] = value;
return true;
}
});
}
console.log('✅ WebSocket monitoring attached');
}
function attachWebSocketEventListeners(ws, url) {
console.log(`🎯 Attaching event listeners to WebSocket for ${url}`);
// Log connection open
ws.addEventListener('open', (event) => {
console.log(`🔓 WebSocket OPEN for ${url}:`, {
readyState: ws.readyState,
url: ws.url,
protocol: ws.protocol,
extensions: ws.extensions
});
});
// Log incoming messages with full details
ws.addEventListener('message', (event) => {
try {
const data = event.data;
console.log(`📨 WebSocket MESSAGE from ${url}:`, {
type: event.type,
data: data,
dataLength: data.length,
timestamp: new Date().toISOString()
});
// Try to parse as JSON for Nostr messages
try {
const parsed = JSON.parse(data);
if (Array.isArray(parsed)) {
const [type, ...args] = parsed;
console.log(`📨 Parsed Nostr message [${type}]:`, args);
} else {
console.log(`📨 Parsed JSON:`, parsed);
}
} catch (parseError) {
console.log(`📨 Raw message (not JSON):`, data);
}
} catch (error) {
console.error(`❌ Error processing WebSocket message from ${url}:`, error);
}
});
// Log connection close with details
ws.addEventListener('close', (event) => {
console.log(`🔒 WebSocket CLOSE for ${url}:`, {
code: event.code,
reason: event.reason,
wasClean: event.wasClean,
readyState: ws.readyState,
timestamp: new Date().toISOString()
});
});
// Log errors with full details
ws.addEventListener('error', (event) => {
console.error(`❌ WebSocket ERROR for ${url}:`, {
type: event.type,
target: event.target,
readyState: ws.readyState,
url: ws.url,
timestamp: new Date().toISOString()
});
// Log additional WebSocket state
console.error(`❌ WebSocket state details:`, {
readyState: ws.readyState,
bufferedAmount: ws.bufferedAmount,
protocol: ws.protocol,
extensions: ws.extensions,
binaryType: ws.binaryType
});
});
// Override send method to log outgoing messages
const originalSend = ws.send;
ws.send = function(data) {
console.log(`📤 WebSocket SEND to ${url}:`, {
data: data,
dataLength: data.length,
readyState: ws.readyState,
timestamp: new Date().toISOString()
});
// Try to parse outgoing Nostr messages
try {
const parsed = JSON.parse(data);
if (Array.isArray(parsed)) {
const [type, ...args] = parsed;
console.log(`📤 Outgoing Nostr message [${type}]:`, args);
} else {
console.log(`📤 Outgoing JSON:`, parsed);
}
} catch (parseError) {
console.log(`📤 Outgoing raw message (not JSON):`, data);
}
return originalSend.call(this, data);
};
console.log(`✅ Event listeners attached to WebSocket for ${url}`);
}
// Configuration subscription using nostr-tools SimplePool
async function subscribeToConfiguration() {
try {
console.log('=== STARTING SIMPLEPOOL CONFIGURATION SUBSCRIPTION ===');
console.log('=== SUBSCRIBE TO CONFIGURATION ===');
console.log('Call stack:', new Error().stack);
// Prevent multiple simultaneous subscription attempts
if (isSubscribed) {
console.log('Subscription already established, skipping duplicate subscription attempt');
// If pool already exists and subscribed, we're done
if (relayPool && isSubscribed) {
console.log('✅ Already subscribed, reusing existing pool');
return true;
}
if (!isLoggedIn) {
console.log('WARNING: Not logged in, but proceeding with subscription test');
}
const url = relayConnectionUrl.value.trim();
if (!url) {
console.error('Please enter a relay URL');
// Prevent concurrent subscription attempts
if (isSubscribing) {
console.log('⚠️ Subscription already in progress');
return false;
}
console.log(`Connecting to relay via SimplePool: ${url}`);
isSubscribing = true;
// Reuse existing pool if available, otherwise create new one
const url = relayConnectionUrl.value.trim();
if (!url) {
console.error('No relay URL configured');
isSubscribing = false;
return false;
}
console.log(`🔌 Connecting to relay: ${url}`);
// Create pool ONLY if it doesn't exist
if (!relayPool) {
console.log('Creating new SimplePool instance');
console.log('Creating NEW SimplePool for admin operations');
relayPool = new window.NostrTools.SimplePool();
// Attach WebSocket monitoring to the new pool
attachWebSocketMonitoring(relayPool, url);
} else {
console.log('Reusing existing SimplePool instance');
console.log('♻️ Reusing existing SimplePool');
}
subscriptionId = generateSubId();
console.log(`Generated subscription ID: ${subscriptionId}`);
console.log(`User pubkey ${userPubkey}`)
console.log(`📝 Generated subscription ID: ${subscriptionId}`);
console.log(`👤 User pubkey: ${userPubkey}`);
console.log(`🎯 About to call relayPool.subscribeMany with URL: ${url}`);
console.log(`📊 relayPool._conn before subscribeMany:`, Object.keys(relayPool._conn || {}));
// Mark as subscribed BEFORE calling subscribeMany to prevent race conditions
isSubscribed = true;
// Subscribe to kind 23457 events (admin response events), kind 4 (NIP-04 DMs), kind 1059 (NIP-17 GiftWrap), and kind 24567 (ephemeral monitoring events)
console.log('🔔 Calling relayPool.subscribeMany...');
const subscription = relayPool.subscribeMany([url], [{
since: Math.floor(Date.now() / 1000) - 5, // Look back 5 seconds to avoid race condition
kinds: [23457],
@@ -872,20 +1052,21 @@ async function subscribeToConfiguration() {
since: Math.floor(Date.now() / 1000), // Start from current time
kinds: [24567], // Real-time ephemeral monitoring events
authors: [getRelayPubkey()], // Only listen to monitoring events from the relay
"#d": isLoggedIn ? ["event_kinds", "time_stats", "top_pubkeys", "active_subscriptions", "subscription_details"] : ["event_kinds", "time_stats", "top_pubkeys", "active_subscriptions"], // Include subscription_details only when authenticated
"#d": isLoggedIn ? ["event_kinds", "time_stats", "top_pubkeys", "active_subscriptions", "subscription_details", "cpu_metrics"] : ["event_kinds", "time_stats", "top_pubkeys", "active_subscriptions", "cpu_metrics"], // Include subscription_details only when authenticated, cpu_metrics available to all
limit: 50
}], {
async onevent(event) {
console.log('=== EVENT RECEIVED VIA SIMPLEPOOL ===');
console.log('Event data:', event);
console.log('Event kind:', event.kind);
console.log('Event tags:', event.tags);
console.log('Event pubkey:', event.pubkey);
console.log('=== END EVENT ===');
// Simplified logging - one line per event
if (event.kind === 24567) {
const dTag = event.tags.find(tag => tag[0] === 'd');
const dataType = dTag ? dTag[1] : 'unknown';
console.log(`📊 Monitoring event: ${dataType}`);
} else {
console.log(`📨 Event received: kind ${event.kind}`);
}
// Handle NIP-04 DMs
if (event.kind === 4) {
console.log('=== NIP-04 DM RECEIVED ===');
try {
// Decrypt the DM content
const decryptedContent = await window.nostr.nip04.decrypt(event.pubkey, event.content);
@@ -910,7 +1091,6 @@ async function subscribeToConfiguration() {
// Handle NIP-17 GiftWrap DMs
if (event.kind === 1059) {
console.log('=== NIP-17 GIFTWRAP RECEIVED ===');
try {
// Step 1: Unwrap gift wrap to get seal
const sealJson = await window.nostr.nip44.decrypt(event.pubkey, event.content);
@@ -958,10 +1138,7 @@ async function subscribeToConfiguration() {
// Handle monitoring events (kind 24567 - ephemeral)
if (event.kind === 24567) {
console.log('=== MONITORING EVENT RECEIVED ===');
console.log('Monitoring event:', event);
// Process monitoring event
// Process monitoring event (logging done above)
processMonitoringEvent(event);
}
},
@@ -975,23 +1152,30 @@ async function subscribeToConfiguration() {
},
onclose(reason) {
console.log('Subscription closed:', reason);
// Reset subscription state to allow re-subscription
isSubscribed = false;
isSubscribing = false;
isRelayConnected = false;
updateConfigStatus(false);
log('WebSocket connection closed - subscription state reset', 'WARNING');
}
});
// Store subscription for cleanup
relayPool.currentSubscription = subscription;
// Mark as subscribed to prevent duplicate attempts
// Mark as subscribed
isSubscribed = true;
isSubscribing = false;
console.log('SimplePool subscription established');
console.log('✅ Subscription established successfully');
return true;
} catch (error) {
console.error('Configuration subscription failed:', error.message);
console.error('Configuration subscription failed:', error);
console.error('Error stack:', error.stack);
isSubscribing = false;
return false;
}
}
@@ -1087,13 +1271,14 @@ function initializeEventRateChart() {
eventRateChart = new ASCIIBarChart('event-rate-chart', {
maxHeight: 11, // Chart height in lines
maxDataPoints: 76, // Show last 76 bins (5+ minutes of history)
title: 'Events', // Chart title
title: 'New Events', // Chart title
xAxisLabel: '', // No X-axis label
yAxisLabel: '', // No Y-axis label
autoFitWidth: true, // Enable responsive font sizing
useBinMode: true, // Enable time bin aggregation
binDuration: 4000, // 4-second time bins
xAxisLabelFormat: 'elapsed' // Show elapsed time labels
xAxisLabelFormat: 'elapsed', // Show elapsed time labels
debug: false // Disable debug logging
});
console.log('ASCIIBarChart instance created:', eventRateChart);
@@ -1148,70 +1333,56 @@ function createChartStubElements() {
// Handle monitoring events (kind 24567 - ephemeral)
async function processMonitoringEvent(event) {
try {
console.log('=== PROCESSING MONITORING EVENT ===');
console.log('Monitoring event:', event);
// Verify this is a kind 24567 ephemeral monitoring event
if (event.kind !== 24567) {
console.log('Ignoring non-monitoring event, kind:', event.kind);
return;
}
// Verify the event is from the relay
const expectedRelayPubkey = getRelayPubkey();
if (event.pubkey !== expectedRelayPubkey) {
console.log('Ignoring monitoring event from unknown pubkey:', event.pubkey);
return;
}
// Check the d-tag to determine which type of monitoring event this is
const dTag = event.tags.find(tag => tag[0] === 'd');
if (!dTag) {
console.log('Ignoring monitoring event without d-tag');
return;
}
// Parse the monitoring data (content is JSON, not encrypted for monitoring events)
const monitoringData = JSON.parse(event.content);
console.log('Parsed monitoring data:', monitoringData);
// Don't add to chart here - we'll track actual event rate changes in updateStatsFromMonitoringEvent
console.log('Monitoring event received - will track rate changes in stats update');
// Route to appropriate handler based on d-tag
// Route to appropriate handler based on d-tag (no verbose logging)
switch (dTag[1]) {
case 'event_kinds':
updateStatsFromMonitoringEvent(monitoringData);
log('Real-time event_kinds monitoring data updated', 'INFO');
break;
case 'time_stats':
updateStatsFromTimeMonitoringEvent(monitoringData);
log('Real-time time_stats monitoring data updated', 'INFO');
break;
case 'top_pubkeys':
updateStatsFromTopPubkeysMonitoringEvent(monitoringData);
log('Real-time top_pubkeys monitoring data updated', 'INFO');
break;
case 'active_subscriptions':
updateStatsFromActiveSubscriptionsMonitoringEvent(monitoringData);
log('Real-time active_subscriptions monitoring data updated', 'INFO');
break;
case 'subscription_details':
// Only process subscription details if user is authenticated
if (isLoggedIn) {
updateStatsFromSubscriptionDetailsMonitoringEvent(monitoringData);
log('Real-time subscription_details monitoring data updated', 'INFO');
} else {
console.log('Ignoring subscription_details monitoring event - user not authenticated');
}
break;
case 'cpu_metrics':
updateStatsFromCpuMonitoringEvent(monitoringData);
break;
default:
console.log('Ignoring monitoring event with unknown d-tag:', dTag[1]);
return;
}
@@ -3770,11 +3941,8 @@ function handleStatsQueryResponse(responseData) {
// Update statistics display from real-time monitoring event
function updateStatsFromMonitoringEvent(monitoringData) {
try {
log('Updating stats from monitoring event...', 'INFO');
console.log('Monitoring data:', monitoringData);
if (monitoringData.data_type !== 'event_kinds') {
log('Ignoring monitoring event with different data type', 'WARNING');
return;
}
@@ -3801,8 +3969,6 @@ function updateStatsFromMonitoringEvent(monitoringData) {
populateStatsKindsFromMonitoring(monitoringData.kinds, monitoringData.total_events);
}
log('Real-time statistics updated from monitoring event', 'INFO');
} catch (error) {
log(`Error updating stats from monitoring event: ${error.message}`, 'ERROR');
}
@@ -3811,11 +3977,7 @@ function updateStatsFromMonitoringEvent(monitoringData) {
// Update statistics display from time_stats monitoring event
function updateStatsFromTimeMonitoringEvent(monitoringData) {
try {
log('Updating time stats from monitoring event...', 'INFO');
console.log('Time monitoring data:', monitoringData);
if (monitoringData.data_type !== 'time_stats') {
log('Ignoring time monitoring event with different data type', 'WARNING');
return;
}
@@ -3834,8 +3996,6 @@ function updateStatsFromTimeMonitoringEvent(monitoringData) {
populateStatsTime({ time_stats: timeStats });
}
log('Real-time time statistics updated from monitoring event', 'INFO');
} catch (error) {
log(`Error updating time stats from monitoring event: ${error.message}`, 'ERROR');
}
@@ -3844,11 +4004,7 @@ function updateStatsFromTimeMonitoringEvent(monitoringData) {
// Update statistics display from top_pubkeys monitoring event
function updateStatsFromTopPubkeysMonitoringEvent(monitoringData) {
try {
log('Updating top pubkeys from monitoring event...', 'INFO');
console.log('Top pubkeys monitoring data:', monitoringData);
if (monitoringData.data_type !== 'top_pubkeys') {
log('Ignoring top pubkeys monitoring event with different data type', 'WARNING');
return;
}
@@ -3858,8 +4014,6 @@ function updateStatsFromTopPubkeysMonitoringEvent(monitoringData) {
populateStatsPubkeysFromMonitoring(monitoringData.pubkeys, monitoringData.total_events || 0);
}
log('Real-time top pubkeys statistics updated from monitoring event', 'INFO');
} catch (error) {
log(`Error updating top pubkeys from monitoring event: ${error.message}`, 'ERROR');
}
@@ -3868,11 +4022,7 @@ function updateStatsFromTopPubkeysMonitoringEvent(monitoringData) {
// Update statistics display from active_subscriptions monitoring event
function updateStatsFromActiveSubscriptionsMonitoringEvent(monitoringData) {
try {
log('Updating active subscriptions from monitoring event...', 'INFO');
console.log('Active subscriptions monitoring data:', monitoringData);
if (monitoringData.data_type !== 'active_subscriptions') {
log('Ignoring active subscriptions monitoring event with different data type', 'WARNING');
return;
}
@@ -3882,8 +4032,6 @@ function updateStatsFromActiveSubscriptionsMonitoringEvent(monitoringData) {
updateStatsCell('active-subscriptions', monitoringData.data.total_subscriptions.toString());
}
log('Real-time active subscriptions statistics updated from monitoring event', 'INFO');
} catch (error) {
log(`Error updating active subscriptions from monitoring event: ${error.message}`, 'ERROR');
}
@@ -3892,11 +4040,7 @@ function updateStatsFromActiveSubscriptionsMonitoringEvent(monitoringData) {
// Update statistics display from subscription_details monitoring event
function updateStatsFromSubscriptionDetailsMonitoringEvent(monitoringData) {
try {
log('Updating subscription details from monitoring event...', 'INFO');
console.log('Subscription details monitoring data:', monitoringData);
if (monitoringData.data_type !== 'subscription_details') {
log('Ignoring subscription details monitoring event with different data type', 'WARNING');
return;
}
@@ -3905,13 +4049,43 @@ function updateStatsFromSubscriptionDetailsMonitoringEvent(monitoringData) {
populateSubscriptionDetailsTable(monitoringData.data.subscriptions);
}
log('Real-time subscription details statistics updated from monitoring event', 'INFO');
} catch (error) {
log(`Error updating subscription details from monitoring event: ${error.message}`, 'ERROR');
}
}
// Update statistics display from CPU metrics monitoring event
function updateStatsFromCpuMonitoringEvent(monitoringData) {
try {
if (monitoringData.data_type !== 'cpu_metrics') {
return;
}
// Update CPU metrics in the database statistics table
if (monitoringData.process_id !== undefined) {
updateStatsCell('process-id', monitoringData.process_id.toString());
}
if (monitoringData.memory_usage_mb !== undefined) {
updateStatsCell('memory-usage', monitoringData.memory_usage_mb.toFixed(1) + ' MB');
}
if (monitoringData.current_cpu_core !== undefined) {
updateStatsCell('cpu-core', 'Core ' + monitoringData.current_cpu_core);
}
// Calculate CPU usage percentage if we have the data
if (monitoringData.process_cpu_time !== undefined && monitoringData.system_cpu_time !== undefined) {
// For now, just show the raw process CPU time (simplified)
// In a real implementation, you'd calculate deltas over time
updateStatsCell('cpu-usage', monitoringData.process_cpu_time + ' ticks');
}
} catch (error) {
log(`Error updating CPU metrics from monitoring event: ${error.message}`, 'ERROR');
}
}
// Populate event kinds table from monitoring data
function populateStatsKindsFromMonitoring(kindsData, totalEvents) {
const tableBody = document.getElementById('stats-kinds-table-body');
@@ -4076,7 +4250,7 @@ function populateSubscriptionDetailsTable(subscriptionsData) {
if (subscriptionsData.length === 0) {
const row = document.createElement('tr');
row.innerHTML = '<td colspan="6" style="text-align: center; font-style: italic;">No active subscriptions</td>';
row.innerHTML = '<td colspan="5" style="text-align: center; font-style: italic;">No active subscriptions</td>';
tableBody.appendChild(row);
return;
}
@@ -4092,8 +4266,8 @@ function populateSubscriptionDetailsTable(subscriptionsData) {
// Format client IP (show full IP for admin view)
const clientIP = subscription.client_ip || 'unknown';
// Format status
const status = subscription.active ? 'Active' : 'Inactive';
// Format wsi_pointer (show full pointer)
const wsiPointer = subscription.wsi_pointer || 'N/A';
// Format filters (show actual filter details)
let filtersDisplay = 'None';
@@ -4161,9 +4335,8 @@ function populateSubscriptionDetailsTable(subscriptionsData) {
row.innerHTML = `
<td style="font-family: 'Courier New', monospace; font-size: 12px;">${subscription.id || 'N/A'}</td>
<td style="font-family: 'Courier New', monospace; font-size: 12px;">${clientIP}</td>
<td style="font-family: 'Courier New', monospace; font-size: 12px;">${wsiPointer}</td>
<td>${durationStr}</td>
<td>${subscription.events_sent || 0}</td>
<td>${status}</td>
<td>${filtersDisplay}</td>
`;
tableBody.appendChild(row);