Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-06-17 00:29:01 +02:00
padre b5e4b9a8ac
commit d59980046b
Se han modificado 10 ficheros con 1048 adiciones y 7 borrados

159
server/README.md Archivo normal
Ver fichero

@@ -0,0 +1,159 @@
# RingNet Dashboard Server
A REST API and web dashboard for monitoring RingNet network topology, nodes, and connections in real-time.
## Features
### 🚀 REST API
- **GET /api/nodes** - List all registered nodes
- **GET /api/nodes/:nodeId** - Get specific node details
- **GET /api/nodes/:nodeId/connections** - Get node connections
- **GET /api/network/stats** - Network statistics
- **GET /api/network/topology** - Ring topology data
- **POST /api/nodes/register** - Register/update node status
- **PUT /api/nodes/:nodeId** - Update node information
### 📊 Web Dashboard
- Real-time network statistics
- Interactive ring topology visualization
- Node status monitoring
- Connection health tracking
- Auto-refresh every 5 seconds
## Quick Start
### 1. Start the Dashboard Server
```bash
# From the ringnet root directory
npm run start:dashboard
# Or with development mode (auto-restart)
npm run dashboard:dev
# Or directly from server directory
cd server
npm start
```
The dashboard will be available at: **http://localhost:3000**
### 2. Start Nodes with Dashboard Reporting
```bash
# Start Oracle node with dashboard reporting
npm run start:oracle -- --port 8080 --dashboard http://localhost:3000
# Start regular nodes
npm run start:node -- --port 8081 --bootstrap localhost:8080 --dashboard http://localhost:3000
npm run start:node -- --port 8082 --bootstrap localhost:8080 --dashboard http://localhost:3000
```
### 3. View the Dashboard
Open your browser to **http://localhost:3000** to see:
- Network statistics (total nodes, connections, oracle nodes)
- Live list of active nodes
- Interactive ring topology visualization
- Real-time status updates
## API Examples
### Get Network Statistics
```bash
curl http://localhost:3000/api/network/stats
```
### Get All Nodes
```bash
curl http://localhost:3000/api/nodes
```
### Get Ring Topology
```bash
curl http://localhost:3000/api/network/topology
```
### Register a Node (used automatically by nodes)
```bash
curl -X POST http://localhost:3000/api/nodes/register \
-H "Content-Type: application/json" \
-d '{
"nodeId": "abc123...",
"port": 8080,
"isOracle": true,
"ringPosition": 0
}'
```
## Configuration
### Environment Variables
- `PORT` - Server port (default: 3000)
### Node Configuration
Nodes automatically report to the dashboard when started with the `--dashboard` parameter:
```bash
--dashboard http://localhost:3000
```
## Dashboard Features
### Network Statistics
- **Total Nodes**: Number of active nodes in the network
- **Oracle Nodes**: Number of Oracle nodes providing enhanced services
- **Total Connections**: Sum of all peer connections
- **Last Updated**: Timestamp of the most recent update
### Node List
- Real-time status indicators (active/inactive)
- Node type identification (Oracle vs Regular)
- Ring position and port information
- Last seen timestamps
### Ring Topology Visualization
- Interactive circular visualization of the ring
- Node positioning based on ring coordinates
- Color-coded Oracle nodes (orange) vs regular nodes (blue)
- Click nodes for detailed information
- Visual representation of the double-ring structure
### Auto-Monitoring
- Nodes automatically register and update their status
- Inactive nodes are removed after 5 minutes of no updates
- Real-time updates every 5 seconds
- Graceful handling of network disconnections
## Dependencies
- **express** - Web server framework
- **cors** - Cross-origin resource sharing
- **ws** - WebSocket support (for future real-time features)
- **chalk** - Colored terminal output
## Development
### Running in Development Mode
```bash
npm run dashboard:dev
```
### API Testing
The dashboard includes a health check endpoint:
```bash
curl http://localhost:3000/health
```
### Custom Styling
Modify `public/index.html` to customize the dashboard appearance. The dashboard uses:
- CSS Grid for responsive layout
- CSS backdrop-filter for glassmorphism effects
- Vanilla JavaScript for API interactions
- SVG-based ring visualization
## Integration
Nodes automatically report their status when the `--dashboard` parameter is provided. The reporting includes:
- Node identification and type
- Ring position and topology
- Connection status and peer information
- Network statistics and health data
The dashboard is designed to be lightweight and can run alongside the ring network without affecting performance.

20
server/package.json Archivo normal
Ver fichero

@@ -0,0 +1,20 @@
{
"name": "ringnet-dashboard-server",
"version": "1.0.0",
"description": "REST API and Dashboard for RingNet Network Monitoring",
"main": "server.js",
"type": "module",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"ws": "^8.16.0",
"chalk": "^5.3.0"
},
"keywords": ["ringnet", "dashboard", "monitoring", "api"],
"author": "",
"license": "MIT"
}

414
server/public/index.html Archivo normal
Ver fichero

@@ -0,0 +1,414 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RingNet Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
min-height: 100vh;
}
.header {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 1rem 2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.header h1 {
color: white;
display: flex;
align-items: center;
gap: 0.5rem;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: rgba(255, 255, 255, 0.9);
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.stat-card h3 {
color: #555;
margin-bottom: 0.5rem;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: #667eea;
}
.content-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.panel {
background: rgba(255, 255, 255, 0.9);
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.panel h2 {
margin-bottom: 1rem;
color: #333;
border-bottom: 2px solid #667eea;
padding-bottom: 0.5rem;
}
.node-list {
max-height: 400px;
overflow-y: auto;
}
.node-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
margin-bottom: 0.5rem;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
border-left: 4px solid #667eea;
}
.node-item.oracle {
border-left-color: #f39c12;
}
.node-info {
display: flex;
flex-direction: column;
}
.node-id {
font-weight: bold;
color: #333;
}
.node-meta {
font-size: 0.8rem;
color: #666;
}
.node-status {
display: flex;
align-items: center;
gap: 0.5rem;
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background: #27ae60;
}
.status-indicator.inactive {
background: #e74c3c;
}
.topology-container {
position: relative;
height: 400px;
background: rgba(255, 255, 255, 0.3);
border-radius: 8px;
overflow: hidden;
}
.ring-visualization {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.ring-circle {
position: relative;
width: 300px;
height: 300px;
border: 3px solid #667eea;
border-radius: 50%;
}
.node-dot {
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
background: #667eea;
border: 3px solid white;
cursor: pointer;
transition: all 0.3s ease;
}
.node-dot.oracle {
background: #f39c12;
}
.node-dot:hover {
transform: scale(1.2);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.node-label {
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
font-size: 0.7rem;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 2px 6px;
border-radius: 4px;
white-space: nowrap;
}
.refresh-btn {
background: #667eea;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
transition: background 0.3s ease;
}
.refresh-btn:hover {
background: #5a6fd8;
}
.loading {
text-align: center;
color: #666;
padding: 2rem;
}
@media (max-width: 768px) {
.content-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
</head>
<body>
<div class="header">
<h1>🔗 RingNet Dashboard</h1>
</div>
<div class="container">
<div class="stats-grid">
<div class="stat-card">
<h3>Total Nodes</h3>
<div class="stat-value" id="total-nodes">-</div>
</div>
<div class="stat-card">
<h3>Oracle Nodes</h3>
<div class="stat-value" id="oracle-nodes">-</div>
</div>
<div class="stat-card">
<h3>Total Connections</h3>
<div class="stat-value" id="total-connections">-</div>
</div>
<div class="stat-card">
<h3>Last Updated</h3>
<div class="stat-value" id="last-updated" style="font-size: 1rem;">-</div>
</div>
</div>
<div class="content-grid">
<div class="panel">
<h2>📋 Active Nodes</h2>
<button class="refresh-btn" onclick="refreshData()">🔄 Refresh</button>
<div id="nodes-list" class="node-list loading">
Loading nodes...
</div>
</div>
<div class="panel">
<h2>🔄 Ring Topology</h2>
<div class="topology-container">
<div class="ring-visualization">
<div class="ring-circle" id="ring-circle">
<div id="topology-loading" class="loading">Loading topology...</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let nodesData = [];
let topologyData = {};
async function fetchData() {
try {
// Fetch network stats
const statsResponse = await fetch('/api/network/stats');
const stats = await statsResponse.json();
updateStats(stats);
// Fetch nodes
const nodesResponse = await fetch('/api/nodes');
nodesData = await nodesResponse.json();
updateNodesList(nodesData);
// Fetch topology
const topologyResponse = await fetch('/api/network/topology');
topologyData = await topologyResponse.json();
updateTopology(topologyData);
} catch (error) {
console.error('Error fetching data:', error);
}
}
function updateStats(stats) {
document.getElementById('total-nodes').textContent = stats.totalNodes;
document.getElementById('oracle-nodes').textContent = stats.oracleNodes;
document.getElementById('total-connections').textContent = stats.totalConnections;
const lastUpdated = new Date(stats.lastUpdated);
document.getElementById('last-updated').textContent = lastUpdated.toLocaleTimeString();
}
function updateNodesList(nodes) {
const nodesList = document.getElementById('nodes-list');
if (nodes.length === 0) {
nodesList.innerHTML = '<div class="loading">No nodes connected</div>';
return;
}
nodesList.innerHTML = nodes.map(node => {
const lastSeen = new Date(node.lastSeen);
const isRecent = Date.now() - lastSeen.getTime() < 2 * 60 * 1000; // 2 minutes
return `
<div class="node-item ${node.isOracle ? 'oracle' : ''}">
<div class="node-info">
<div class="node-id">${node.nodeId.substring(0, 12)}...</div>
<div class="node-meta">
${node.isOracle ? '🔮 Oracle' : '💾 Node'} |
Position: ${node.ringPosition || 'N/A'} |
Port: ${node.port || 'N/A'}
</div>
</div>
<div class="node-status">
<div class="status-indicator ${isRecent ? '' : 'inactive'}"></div>
<span>${isRecent ? 'Active' : 'Inactive'}</span>
</div>
</div>
`;
}).join('');
}
function updateTopology(topology) {
const ringCircle = document.getElementById('ring-circle');
const loadingDiv = document.getElementById('topology-loading');
if (loadingDiv) {
loadingDiv.remove();
}
// Clear existing nodes
ringCircle.querySelectorAll('.node-dot').forEach(dot => dot.remove());
if (!topology.nodes || topology.nodes.length === 0) {
ringCircle.innerHTML = '<div class="loading">No topology data</div>';
return;
}
const ringRadius = 140; // Radius for node positioning
const centerX = 150;
const centerY = 150;
topology.nodes.forEach(node => {
// Calculate position on circle
const angle = (node.position / topology.ringSize) * 2 * Math.PI - Math.PI / 2;
const x = centerX + ringRadius * Math.cos(angle);
const y = centerY + ringRadius * Math.sin(angle);
// Create node dot
const nodeDot = document.createElement('div');
nodeDot.className = `node-dot ${node.isOracle ? 'oracle' : ''}`;
nodeDot.style.left = `${x - 10}px`;
nodeDot.style.top = `${y - 10}px`;
// Add label
const label = document.createElement('div');
label.className = 'node-label';
label.textContent = node.shortId;
nodeDot.appendChild(label);
// Add click handler for node details
nodeDot.addEventListener('click', () => {
showNodeDetails(node);
});
ringCircle.appendChild(nodeDot);
});
}
function showNodeDetails(node) {
alert(`Node Details:
ID: ${node.nodeId}
Position: ${node.position}
Type: ${node.isOracle ? 'Oracle' : 'Regular'}
Status: ${node.status}
Connections: ${node.connections.length}`);
}
function refreshData() {
fetchData();
}
// Initial load
fetchData();
// Auto-refresh every 5 seconds
setInterval(fetchData, 5000);
</script>
</body>
</html>

279
server/server.js Archivo normal
Ver fichero

@@ -0,0 +1,279 @@
import express from 'express';
import cors from 'cors';
import WebSocket from 'ws';
import chalk from 'chalk';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
class RingNetDashboardServer {
constructor(port = 3000) {
this.port = port;
this.app = express();
this.nodes = new Map(); // Store connected nodes information
this.networkStats = {
totalNodes: 0,
totalConnections: 0,
oracleNodes: 0,
lastUpdated: new Date()
};
this.setupMiddleware();
this.setupRoutes();
this.setupWebSocketConnections();
}
setupMiddleware() {
this.app.use(cors());
this.app.use(express.json());
this.app.use(express.static(join(__dirname, 'public')));
}
setupRoutes() {
// API Routes
this.app.get('/api/nodes', (req, res) => {
const nodes = Array.from(this.nodes.values()).map(node => ({
...node,
lastSeen: new Date(node.lastSeen).toISOString()
}));
res.json(nodes);
});
this.app.get('/api/network/stats', (req, res) => {
res.json({
...this.networkStats,
lastUpdated: this.networkStats.lastUpdated.toISOString()
});
});
this.app.get('/api/network/topology', (req, res) => {
const topology = this.generateNetworkTopology();
res.json(topology);
});
this.app.get('/api/nodes/:nodeId', (req, res) => {
const node = this.nodes.get(req.params.nodeId);
if (!node) {
return res.status(404).json({ error: 'Node not found' });
}
res.json({
...node,
lastSeen: new Date(node.lastSeen).toISOString()
});
});
this.app.get('/api/nodes/:nodeId/connections', (req, res) => {
const node = this.nodes.get(req.params.nodeId);
if (!node) {
return res.status(404).json({ error: 'Node not found' });
}
res.json(node.connections || []);
});
// Dashboard route
this.app.get('/', (req, res) => {
res.sendFile(join(__dirname, 'public', 'index.html'));
});
// Health check
this.app.get('/health', (req, res) => {
res.json({
status: 'healthy',
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
});
}
setupWebSocketConnections() {
// This could be used to connect to ring nodes and receive real-time updates
// For now, we'll simulate or wait for nodes to report their status
}
generateNetworkTopology() {
const nodes = Array.from(this.nodes.values());
const ringPositions = nodes
.filter(node => node.ringPosition !== undefined)
.sort((a, b) => a.ringPosition - b.ringPosition);
return {
totalNodes: nodes.length,
ringSize: 1000, // Default ring size
nodes: ringPositions.map(node => ({
nodeId: node.nodeId,
shortId: node.nodeId.substring(0, 8),
position: node.ringPosition,
isOracle: node.isOracle || false,
status: node.status || 'unknown',
connections: node.connectedPeers || [],
lastSeen: node.lastSeen
})),
connections: this.generateConnectionMap(nodes)
};
}
generateConnectionMap(nodes) {
const connections = [];
nodes.forEach(node => {
if (node.rings) {
// Inner ring connections
if (node.rings.inner) {
if (node.rings.inner.left) {
connections.push({
from: node.nodeId,
to: node.rings.inner.left,
type: 'inner-left'
});
}
if (node.rings.inner.right) {
connections.push({
from: node.nodeId,
to: node.rings.inner.right,
type: 'inner-right'
});
}
}
// Outer ring connections
if (node.rings.outer) {
if (node.rings.outer.left) {
connections.push({
from: node.nodeId,
to: node.rings.outer.left,
type: 'outer-left'
});
}
if (node.rings.outer.right) {
connections.push({
from: node.nodeId,
to: node.rings.outer.right,
type: 'outer-right'
});
}
}
}
});
return connections;
}
// Method for nodes to register themselves
registerNode(nodeInfo) {
const nodeId = nodeInfo.nodeId;
this.nodes.set(nodeId, {
...nodeInfo,
lastSeen: Date.now(),
status: 'active'
});
this.updateNetworkStats();
console.log(chalk.green(`📊 Node registered: ${nodeId.substring(0, 8)}...`));
}
// Method for nodes to update their status
updateNode(nodeId, updates) {
const node = this.nodes.get(nodeId);
if (node) {
this.nodes.set(nodeId, {
...node,
...updates,
lastSeen: Date.now()
});
this.updateNetworkStats();
}
}
// Method to remove inactive nodes
removeInactiveNodes() {
const now = Date.now();
const timeout = 5 * 60 * 1000; // 5 minutes timeout
for (const [nodeId, node] of this.nodes.entries()) {
if (now - node.lastSeen > timeout) {
this.nodes.delete(nodeId);
console.log(chalk.yellow(`🕐 Removed inactive node: ${nodeId.substring(0, 8)}...`));
}
}
this.updateNetworkStats();
}
updateNetworkStats() {
const nodes = Array.from(this.nodes.values());
this.networkStats = {
totalNodes: nodes.length,
totalConnections: nodes.reduce((sum, node) => sum + (node.connectedPeers?.length || 0), 0),
oracleNodes: nodes.filter(node => node.isOracle).length,
lastUpdated: new Date()
};
}
start() {
// Start cleanup interval for inactive nodes
setInterval(() => {
this.removeInactiveNodes();
}, 60000); // Check every minute
this.server = this.app.listen(this.port, () => {
console.log(chalk.blue(`
🚀 RingNet Dashboard Server Started!
📊 Dashboard: http://localhost:${this.port}
🔗 API: http://localhost:${this.port}/api
📈 Health: http://localhost:${this.port}/health
Monitoring ${this.networkStats.totalNodes} nodes...
`));
});
return this.server;
}
stop() {
if (this.server) {
this.server.close();
console.log(chalk.yellow('🛑 Dashboard server stopped'));
}
}
}
// API endpoint for nodes to report their status
const dashboardServer = new RingNetDashboardServer(process.env.PORT || 3000);
// Add POST endpoint for nodes to register/update
dashboardServer.app.post('/api/nodes/register', (req, res) => {
try {
const nodeInfo = req.body;
dashboardServer.registerNode(nodeInfo);
res.json({ success: true, message: 'Node registered successfully' });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
dashboardServer.app.put('/api/nodes/:nodeId', (req, res) => {
try {
const nodeId = req.params.nodeId;
const updates = req.body;
dashboardServer.updateNode(nodeId, updates);
res.json({ success: true, message: 'Node updated successfully' });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Start the server
dashboardServer.start();
// Graceful shutdown
process.on('SIGINT', () => {
console.log(chalk.yellow('\n🛑 Shutting down dashboard server...'));
dashboardServer.stop();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log(chalk.yellow('\n🛑 Shutting down dashboard server...'));
dashboardServer.stop();
process.exit(0);
});
export default RingNetDashboardServer;