diff --git a/README.md b/README.md
index ca9bd6f..9a58615 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,8 @@ This frontend server works together with the **SPORE Gateway** (spore-gateway) b

### Topology

+### Events
+
### Monitoring

### Firmware
diff --git a/assets/events.png b/assets/events.png
new file mode 100644
index 0000000..98f93e0
Binary files /dev/null and b/assets/events.png differ
diff --git a/assets/firmware.png b/assets/firmware.png
index dfca4be..92d7a3b 100644
Binary files a/assets/firmware.png and b/assets/firmware.png differ
diff --git a/docs/Events.md b/docs/Events.md
new file mode 100644
index 0000000..815ae8a
--- /dev/null
+++ b/docs/Events.md
@@ -0,0 +1,518 @@
+# Events Feature
+
+## Overview
+
+The Events feature provides real-time visualization of WebSocket events streaming through the SPORE cluster. It displays events as an interactive force-directed graph, showing the flow and relationships between different event topics.
+
+
+
+## Features
+
+### Real-Time Event Visualization
+- **Interactive Graph**: Events are visualized as a force-directed graph using D3.js
+- **Topic Chain Visualization**: Multi-part topics (e.g., `cluster/event/api/neopattern`) are broken down into chains showing hierarchical relationships
+- **Event Counting**: Each event type is tracked with occurrence counts displayed on connections
+- **Animated Transitions**: New events trigger visual animations showing data flow through the graph
+
+### User Interactions
+
+#### Drag to Reposition Nodes
+- **Center Node**: Dragging the center (blue) node moves the entire graph together
+- **Topic Nodes**: Individual topic nodes (green) can be repositioned independently
+- **Fixed Positioning**: Once dragged, nodes maintain their positions to prevent layout disruption
+
+#### Zoom Controls
+- **Mouse Wheel**: Zoom in and out using the mouse wheel
+- **Scale Range**: Zoom level constrained between 0.5x and 5x
+- **Pan**: Click and drag the canvas to pan around the graph
+
+#### Rearrange Layout
+- **Rearrange Button**: Click the button in the top-left corner to reset node positions
+- **Automatic Layout**: Clears fixed positions and lets the force simulation reposition nodes
+
+### Visual Elements
+
+#### Node Types
+- **Center Node** (Blue):
+ - Represents the central event hub
+ - Always visible in the graph
+ - Acts as the root node for all event chains
+ - Larger size (18px radius)
+
+- **Topic Nodes** (Green):
+ - Represent individual topic segments
+ - Examples: `cluster`, `event`, `api`, `neopattern`
+ - Smaller size (14px radius)
+ - Positioned dynamically based on event frequency
+
+#### Link Types
+- **Center Links** (Blue):
+ - Connect the center node to root topic nodes
+ - Thicker lines (2.5px)
+ - Examples: center → `cluster`, center → `event`
+
+- **Topic Links** (Green):
+ - Connect adjacent topics in event chains
+ - Line thickness proportional to event frequency
+ - Examples: `cluster` → `event`, `api` → `neopattern`
+
+#### Hover Interactions
+- **Link Hover**: Links become thicker and more opaque when hovered
+- **Event Frequency**: Line thickness dynamically adjusts based on event occurrence counts
+
+## Architecture
+
+### Component Structure
+
+```12:37:spore-ui/public/scripts/components/EventComponent.js
+// Events Component - Visualizes websocket events as a graph
+class EventComponent extends Component {
+ constructor(container, viewModel, eventBus) {
+ super(container, viewModel, eventBus);
+
+ this.svg = null;
+ this.simulation = null;
+ this.zoom = null;
+ this.width = 0;
+ this.height = 0;
+ this.isInitialized = false;
+
+ // Center node data - will be initialized with proper coordinates later
+ this.centerNode = { id: 'center', type: 'center', label: '', x: 0, y: 0 };
+
+ // Track nodes for D3
+ this.graphNodes = [];
+ this.graphLinks = [];
+
+ // Track recent events to trigger animations
+ this.lastSeenEvents = new Set();
+ }
+```
+
+### ViewModel
+
+The `EventViewModel` manages the state and WebSocket connections:
+
+```1241:1253:spore-ui/public/scripts/view-models.js
+// 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();
+ }
+```
+
+### Event Tracking
+
+Events are tracked with the following metadata:
+- **Topic**: The full event topic path (e.g., `cluster/event/api/neopattern`)
+- **Parts**: Array of topic segments split by `/`
+- **Count**: Total occurrences of this event
+- **First Seen**: ISO timestamp of first occurrence
+- **Last Seen**: ISO timestamp of most recent occurrence
+- **Last Data**: The most recent event payload
+
+### Event Addition Logic
+
+The view model handles nested events specially:
+
+```1283:1329:spore-ui/public/scripts/view-models.js
+ // 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());
+ }
+```
+
+## Graph Construction
+
+### Node Creation
+
+The graph construction process follows two passes:
+
+1. **First Pass - Node Creation**:
+ - Creates nodes for each unique topic segment
+ - Preserves existing node positions if they've been dragged
+ - New nodes are positioned near their parent in the hierarchy
+
+2. **Second Pass - Event Counting**:
+ - Counts occurrences of each topic segment
+ - Updates node counts to reflect event frequency
+
+### Link Construction
+
+Links are created as chains representing complete topic paths:
+
+```283:342:spore-ui/public/scripts/components/EventComponent.js
+ // Build links as chains for each topic
+ // For "cluster/update", create: center -> cluster -> update
+ this.graphLinks = [];
+ const linkSet = new Set(); // Track links to avoid duplicates
+
+ if (events && events.size > 0) {
+ for (const [topic, data] of events) {
+ const parts = data.parts;
+
+ if (parts.length === 0) continue;
+
+ // Connect center to first part
+ const firstPart = parts[0];
+ const centerToFirst = `center-${firstPart}`;
+ if (!linkSet.has(centerToFirst)) {
+ this.graphLinks.push({
+ source: 'center',
+ target: firstPart,
+ type: 'center-link',
+ count: data.count,
+ topic: topic
+ });
+ linkSet.add(centerToFirst);
+ } else {
+ // Update count for existing link
+ const existingLink = this.graphLinks.find(l =>
+ `${l.source}-${l.target}` === centerToFirst
+ );
+ if (existingLink) {
+ existingLink.count += data.count;
+ }
+ }
+
+ // Connect each part to the next (creating a chain)
+ for (let i = 0; i < parts.length - 1; i++) {
+ const source = parts[i];
+ const target = parts[i + 1];
+ const linkKey = `${source}-${target}`;
+
+ if (!linkSet.has(linkKey)) {
+ this.graphLinks.push({
+ source: source,
+ target: target,
+ type: 'topic-link',
+ topic: topic,
+ count: data.count
+ });
+ linkSet.add(linkKey);
+ } else {
+ // Update count for existing link
+ const existingLink = this.graphLinks.find(l =>
+ `${l.source}-${l.target}` === linkKey
+ );
+ if (existingLink) {
+ existingLink.count += data.count;
+ }
+ }
+ }
+ }
+ }
+```
+
+## Force Simulation
+
+The D3.js force simulation provides the physics-based layout:
+
+```103:111:spore-ui/public/scripts/components/EventComponent.js
+ // Initialize simulation
+ this.simulation = d3.forceSimulation()
+ .force('link', d3.forceLink().id(d => d.id).distance(100))
+ .force('charge', d3.forceManyBody().strength(-300))
+ .force('center', d3.forceCenter(this.width / 2, this.height / 2))
+ .force('collision', d3.forceCollide().radius(35))
+ .alphaDecay(0.0228) // Slower decay to allow simulation to run longer
+ .velocityDecay(0.4); // Higher velocity decay for smoother, less jumpy movement
+```
+
+### Force Parameters
+- **Link Force**: Maintains 100px distance between connected nodes
+- **Charge Force**: -300 strength creates repulsion between nodes
+- **Center Force**: Keeps the graph centered in the viewport
+- **Collision Detection**: Prevents nodes from overlapping (35px collision radius)
+- **Decay Rate**: Slow decay (0.0228) keeps the simulation running smoothly
+- **Velocity Decay**: High decay (0.4) prevents excessive movement
+
+## Animation System
+
+### Event Arrival Animation
+
+When a new event arrives, a golden animation dot travels along the event chain:
+
+```725:809:spore-ui/public/scripts/components/EventComponent.js
+ animateEventMessage(topic, data) {
+ const g = this.svg.select('g');
+ if (g.empty()) return;
+
+ const parts = data.parts || topic.split('/').filter(p => p);
+ if (parts.length === 0) return;
+
+ // Wait for the next tick to ensure nodes exist
+ setTimeout(() => {
+ // Build the chain: center -> first -> second -> ... -> last
+ const chain = ['center', ...parts];
+
+ // Animate along each segment of the chain
+ let delay = 0;
+ for (let i = 0; i < chain.length - 1; i++) {
+ const sourceId = chain[i];
+ const targetId = chain[i + 1];
+
+ const sourceNode = this.graphNodes.find(n => n.id === sourceId);
+ const targetNode = this.graphNodes.find(n => n.id === targetId);
+
+ if (!sourceNode || !targetNode) continue;
+
+ setTimeout(() => {
+ this.animateDotAlongLink(sourceNode, targetNode, g);
+ }, delay);
+
+ // Add delay between segments (staggered animation)
+ delay += 400; // Duration (300ms) + small gap (100ms)
+ }
+ }, 100);
+ }
+
+ animateDotAlongLink(sourceNode, targetNode, g) {
+ if (!sourceNode || !targetNode || !g) return;
+
+ // Calculate positions
+ const dx = targetNode.x - sourceNode.x;
+ const dy = targetNode.y - sourceNode.y;
+ const length = Math.sqrt(dx * dx + dy * dy);
+
+ if (length === 0) return;
+
+ // Normalize direction
+ const nx = dx / length;
+ const ny = dy / length;
+
+ // Calculate node radii
+ const sourceRadius = sourceNode.type === 'center' ? 18 : 14;
+ const targetRadius = targetNode.type === 'center' ? 18 : 14;
+
+ // Start from edge of source node
+ const startX = sourceNode.x + nx * sourceRadius;
+ const startY = sourceNode.y + ny * sourceRadius;
+
+ // End at edge of target node
+ const endX = targetNode.x - nx * targetRadius;
+ const endY = targetNode.y - ny * targetRadius;
+
+ // Create animation dot
+ const animationGroup = g.append('g').attr('class', 'animation-group');
+
+ const dot = animationGroup.append('circle')
+ .attr('r', 4)
+ .attr('fill', '#FFD700')
+ .attr('stroke', '#fff')
+ .attr('stroke-width', 2)
+ .attr('cx', startX)
+ .attr('cy', startY);
+
+ // Animate the dot along the path
+ dot.transition()
+ .duration(300)
+ .ease(d3.easeLinear)
+ .attr('cx', endX)
+ .attr('cy', endY)
+ .on('end', function() {
+ // Fade out after reaching destination
+ dot.transition()
+ .duration(100)
+ .style('opacity', 0)
+ .remove();
+ animationGroup.remove();
+ });
+ }
+```
+
+### Animation Characteristics
+- **Golden Dot**: Visual indicator traveling along event chains
+- **Staggered Timing**: Each segment of a multi-part topic animates sequentially
+- **Duration**: 300ms per segment with 100ms delay between segments
+- **Smooth Motion**: Linear easing for consistent speed
+
+## WebSocket Integration
+
+The Events view subscribes to all WebSocket messages through the gateway:
+
+```1256:1280:spore-ui/public/scripts/view-models.js
+ // 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');
+ });
+ }
+```
+
+### Event Types Tracked
+
+All WebSocket messages are captured and displayed, including:
+- **cluster/update**: Node discovery and cluster membership changes
+- **cluster/event**: Arbitrary events from cluster members
+- **api/tasks**: Task execution notifications
+- **api/config**: Configuration updates
+- **node/***: Node-specific events
+- Any custom topics sent by SPORE nodes
+
+## Empty State
+
+When no events have been received, the view displays a helpful message:
+
+```604:632:spore-ui/public/scripts/components/EventComponent.js
+ showEmptyState() {
+ if (!this.isInitialized || !this.svg) return;
+
+ const g = this.svg.select('g');
+ if (g.empty()) return;
+
+ // Remove any existing message
+ g.selectAll('.empty-state').remove();
+
+ // Account for the initial zoom transform (scale(1.4) translate(-200, -150))
+ // We need to place the text in the center of the transformed coordinate space
+ const transformX = -200;
+ const transformY = -150;
+ const scale = 1.4;
+
+ // Calculate centered position in transformed space
+ const x = ((this.width / 2) - transformX) / scale;
+ const y = ((this.height / 2) - transformY) / scale;
+
+ const emptyMsg = g.append('text')
+ .attr('class', 'empty-state')
+ .attr('x', x)
+ .attr('y', y)
+ .attr('text-anchor', 'middle')
+ .attr('font-size', '18px')
+ .attr('fill', 'var(--text-secondary)')
+ .attr('font-weight', '500')
+ .text('Waiting for websocket events...');
+ }
+```
+
+## Best Practices
+
+### Performance Considerations
+1. **Event Accumulation**: Events accumulate over time; consider clearing the view for long-running sessions
+2. **Many Events**: With hundreds of unique topics, the graph may become cluttered
+3. **Animation**: Rapid event bursts may cause overlapping animations
+
+### Usage Tips
+1. **Rearrange Regularly**: Use the rearrange button to clean up the layout after many events
+2. **Drag Center Node**: Move the entire graph to see different areas of the visualization
+3. **Zoom Out**: Zoom out to see the overall event pattern across the cluster
+4. **Interactive Exploration**: Hover over links to see event frequencies
+
+## Troubleshooting
+
+### No Events Showing
+- **Check WebSocket Connection**: Verify the gateway WebSocket is connected
+- **Check Node Activity**: Ensure SPORE nodes are actively sending events
+- **Browser Console**: Check for any JavaScript errors
+
+### Graph Not Updating
+- **Force Refresh**: Reload the page to reinitialize the WebSocket connection
+- **Check Gateway**: Verify the SPORE Gateway is running and accessible
+
+### Performance Issues
+- **Clear Events**: Reload the page to clear accumulated events
+- **Reduce Events**: Limit the event rate in your SPORE nodes
+- **Browser Settings**: Ensure hardware acceleration is enabled
+
+## Technical Details
+
+### Dependencies
+- **D3.js v7**: Force-directed graph simulation and SVG manipulation
+- **SPORE Gateway**: WebSocket connection to cluster event stream
+- **Component Framework**: Built on the SPORE UI component architecture
+
+### Browser Compatibility
+- **Modern Browsers**: Chrome, Firefox, Safari, Edge (latest versions)
+- **SVG Support**: Requires modern browser with full SVG support
+- **WebSocket Support**: Native WebSocket API required
+
+### View Structure
+
+```254:262:spore-ui/public/index.html
+
+
+
+
+
Waiting for websocket events...
+
+
+
+
+```
+
+The Events view provides a unique perspective on cluster activity, making it easy to observe the flow of events through the SPORE system in real-time.
+
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..d94e548
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,78 @@
+# SPORE UI Documentation
+
+This directory contains detailed documentation for the SPORE UI frontend application.
+
+## Documentation Index
+
+### Core Documentation
+
+- **[Framework](./FRAMEWORK_README.md)**: Component-based architecture, View Models, Event Bus, and framework conventions
+- **[Discovery](./DISCOVERY.md)**: Node discovery system and cluster management
+- **[Topology WebSocket Update](./TOPOLOGY_WEBSOCKET_UPDATE.md)**: Real-time topology visualization updates
+- **[Logging](./LOGGING.md)**: Logging system and debugging utilities
+
+### Feature Documentation
+
+- **[Events](./Events.md)**: Real-time WebSocket event visualization with interactive force-directed graph
+- **[Firmware Registry Integration](./FIRMWARE_REGISTRY_INTEGRATION.md)**: Integration with the firmware registry system
+
+## Feature Overview
+
+### Events Feature
+The Events feature provides real-time visualization of WebSocket events streaming through the SPORE cluster. It displays events as an interactive force-directed graph, showing the flow and relationships between different event topics.
+
+**Key Capabilities:**
+- Real-time event tracking and visualization
+- Interactive force-directed graph layout
+- Event counting and frequency visualization
+- Animated event flow visualization
+- Topic hierarchy display
+
+See [Events.md](./Events.md) for complete documentation.
+
+### Framework Architecture
+The SPORE UI uses a component-based architecture with:
+- View Models for state management
+- Components for UI rendering
+- Event Bus for pub/sub communication
+- API Client for backend integration
+
+See [FRAMEWORK_README.md](./FRAMEWORK_README.md) for architecture details.
+
+### Discovery System
+The discovery system enables automatic detection of SPORE nodes on the network, maintaining a real-time view of cluster members.
+
+See [DISCOVERY.md](./DISCOVERY.md) for implementation details.
+
+### Topology Visualization
+The topology view provides an interactive network graph showing relationships between cluster nodes in real-time.
+
+See [TOPOLOGY_WEBSOCKET_UPDATE.md](./TOPOLOGY_WEBSOCKET_UPDATE.md) for detailed documentation.
+
+## Quick Reference
+
+### Most Popular Pages
+1. [Framework Architecture](./FRAMEWORK_README.md) - Start here for understanding the codebase
+2. [Events Feature](./Events.md) - Real-time event visualization
+3. [Firmware Registry Integration](./FIRMWARE_REGISTRY_INTEGRATION.md) - Firmware management
+
+### Developer Resources
+- Component development: See [FRAMEWORK_README.md](./FRAMEWORK_README.md)
+- Debugging: See [LOGGING.md](./LOGGING.md)
+- Testing: See individual component documentation
+
+## Contributing
+
+When adding new features or documentation:
+1. Follow the existing documentation structure
+2. Include code examples and diagrams where helpful
+3. Update this README when adding new documentation files
+4. Maintain consistency with existing documentation style
+
+## Related Documentation
+
+For information about other SPORE components:
+- **SPORE Gateway**: See `spore-gateway/README.md`
+- **SPORE Registry**: See `spore-registry/README.md`
+- **SPORE Embedded**: See `spore/README.md`
+
diff --git a/public/index.html b/public/index.html
index 15c0a96..2f0b9cd 100644
--- a/public/index.html
+++ b/public/index.html
@@ -39,6 +39,18 @@
Topology
+
+
+ Events
+