feat: events visualization

This commit is contained in:
2025-10-26 13:40:09 +01:00
parent 359d681c72
commit f6dc4e8bf3
10 changed files with 1591 additions and 9 deletions

View File

@@ -1235,4 +1235,147 @@ class WiFiConfigViewModel extends ViewModel {
}
}
window.WiFiConfigViewModel = WiFiConfigViewModel;
window.WiFiConfigViewModel = WiFiConfigViewModel;
// Events View Model for websocket event visualization
class EventViewModel extends ViewModel {
constructor() {
super();
this.setMultiple({
events: new Map(), // Map of topic -> { parts: [], count: number, lastSeen: timestamp }
isLoading: false,
error: null,
lastUpdateTime: null
});
// Set up WebSocket listeners for real-time updates
this.setupWebSocketListeners();
}
// Set up WebSocket event listeners
setupWebSocketListeners() {
if (!window.wsClient) {
// Retry after a short delay to allow wsClient to initialize
setTimeout(() => this.setupWebSocketListeners(), 1000);
return;
}
// Listen for all websocket messages
window.wsClient.on('message', (data) => {
const topic = data.topic || data.type;
if (topic) {
this.addTopic(topic, data);
}
});
// Listen for connection status changes
window.wsClient.on('connected', () => {
logger.info('EventViewModel: WebSocket connected');
});
window.wsClient.on('disconnected', () => {
logger.debug('EventViewModel: WebSocket disconnected');
});
}
// Add a topic (parsed by "/" separator)
addTopic(topic, data = null) {
// Get current events as a new Map to ensure change detection
const events = new Map(this.get('events'));
// Handle nested events from cluster/event
let fullTopic = topic;
if (topic === 'cluster/event' && data && data.data) {
try {
const parsedData = typeof data.data === 'string' ? JSON.parse(data.data) : data.data;
if (parsedData && parsedData.event) {
// Create nested topic chain: cluster/event/api/neopattern
fullTopic = `${topic}/${parsedData.event}`;
}
} catch (e) {
// If parsing fails, just use the original topic
}
}
const parts = fullTopic.split('/').filter(p => p);
if (events.has(fullTopic)) {
// Update existing event - create new object to ensure change detection
const existing = events.get(fullTopic);
events.set(fullTopic, {
topic: existing.topic,
parts: existing.parts,
count: existing.count + 1,
firstSeen: existing.firstSeen,
lastSeen: new Date().toISOString(),
lastData: data
});
} else {
// Create new event entry
events.set(fullTopic, {
topic: fullTopic,
parts: parts,
count: 1,
firstSeen: new Date().toISOString(),
lastSeen: new Date().toISOString(),
lastData: data
});
}
// Use set to trigger change notification
this.set('events', events);
this.set('lastUpdateTime', new Date().toISOString());
}
// Get all topic parts (unique segments after splitting by "/")
getTopicParts() {
const events = this.get('events');
const allParts = new Set();
for (const [topic, data] of events) {
data.parts.forEach(part => allParts.add(part));
}
return Array.from(allParts).map(part => {
// Count how many events contain this part
let count = 0;
for (const [topic, data] of events) {
if (data.parts.includes(part)) {
count += data.count;
}
}
return { part, count };
});
}
// Clear all events
clearTopics() {
this.set('events', new Map());
this.set('lastUpdateTime', new Date().toISOString());
}
// Get connections between topic parts (for graph edges)
getTopicConnections() {
const topics = this.get('topics');
const connections = [];
for (const [topic, data] of topics) {
const parts = data.parts;
// Create connections between adjacent parts
for (let i = 0; i < parts.length - 1; i++) {
connections.push({
source: parts[i],
target: parts[i + 1],
topic: topic,
count: data.count
});
}
}
return connections;
}
}
window.EventViewModel = EventViewModel;