47
README.md
47
README.md
@@ -50,23 +50,29 @@ npm install
|
||||
|
||||
### Running the Network
|
||||
|
||||
1. **Start the first node (Oracle) as bootstrap:**
|
||||
1. **Start the Dashboard (Optional):**
|
||||
```bash
|
||||
npm run start:oracle -- --port 8080
|
||||
npm run start:dashboard
|
||||
```
|
||||
Dashboard available at: http://localhost:3000
|
||||
|
||||
2. **Start the first node (Oracle) as bootstrap:**
|
||||
```bash
|
||||
npm run start:oracle -- --port 8080 --dashboard http://localhost:3000
|
||||
```
|
||||
|
||||
2. **Start additional nodes:**
|
||||
3. **Start additional nodes:**
|
||||
```bash
|
||||
# Regular node
|
||||
npm run start:node -- --port 8081 --bootstrap localhost:8080
|
||||
npm run start:node -- --port 8081 --bootstrap localhost:8080 --dashboard http://localhost:3000
|
||||
|
||||
# Another Oracle node
|
||||
npm run start:oracle -- --port 8082 --bootstrap localhost:8080
|
||||
npm run start:oracle -- --port 8082 --bootstrap localhost:8080 --dashboard http://localhost:3000
|
||||
```
|
||||
|
||||
3. **Start more nodes:**
|
||||
4. **Start more nodes:**
|
||||
```bash
|
||||
npm run start:node -- --port 8083 --bootstrap localhost:8080
|
||||
npm run start:node -- --port 8083 --bootstrap localhost:8080 --dashboard http://localhost:3000
|
||||
```
|
||||
|
||||
## 📖 Usage
|
||||
@@ -77,6 +83,7 @@ npm run start:node -- --port 8083 --bootstrap localhost:8080
|
||||
--port <port> # Port to listen on (default: random)
|
||||
--id <id> # Node ID (default: auto-generated UUID)
|
||||
--bootstrap <addr> # Bootstrap node address (host:port)
|
||||
--dashboard <url> # Dashboard server URL (default: http://localhost:3000)
|
||||
--ice-servers <json> # ICE servers configuration (JSON array)
|
||||
--config <file> # Load configuration from JSON file
|
||||
--help # Show help message
|
||||
@@ -211,6 +218,32 @@ The Ring Network now features **automatic node positioning** that optimizes netw
|
||||
|
||||
Use the `topology` command in any node to view the current ring structure and positions.
|
||||
|
||||
## 📊 Dashboard & Monitoring
|
||||
|
||||
The RingNet includes a web-based dashboard for real-time network monitoring:
|
||||
|
||||
### Features
|
||||
- **Real-time Network Statistics**: Total nodes, connections, Oracle nodes
|
||||
- **Interactive Ring Topology**: Visual representation of the ring structure
|
||||
- **Node Status Monitoring**: Active/inactive status, connection health
|
||||
- **REST API**: Programmatic access to network data
|
||||
|
||||
### Quick Start
|
||||
```bash
|
||||
# Start the dashboard server
|
||||
npm run start:dashboard
|
||||
|
||||
# Dashboard available at: http://localhost:3000
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
- `GET /api/nodes` - List all nodes
|
||||
- `GET /api/network/stats` - Network statistics
|
||||
- `GET /api/network/topology` - Ring topology data
|
||||
- `GET /api/nodes/:nodeId` - Node details
|
||||
|
||||
See `server/README.md` for complete API documentation.
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
Run the test suite to verify network functionality:
|
||||
|
||||
62
demo-dashboard.js
Archivo normal
62
demo-dashboard.js
Archivo normal
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import chalk from 'chalk';
|
||||
|
||||
console.log(chalk.blue(`
|
||||
📊 RingNet Dashboard Demo
|
||||
|
||||
This demonstration shows how to use the RingNet dashboard
|
||||
to monitor your network in real-time.
|
||||
|
||||
🚀 Step 1: Start the Dashboard Server
|
||||
${chalk.green('npm run start:dashboard')}
|
||||
|
||||
The dashboard will be available at: ${chalk.cyan('http://localhost:3000')}
|
||||
|
||||
🔗 Step 2: Start Nodes with Dashboard Integration
|
||||
${chalk.green('npm run start:oracle -- --port 8080 --dashboard http://localhost:3000')}
|
||||
${chalk.green('npm run start:node -- --port 8081 --bootstrap localhost:8080 --dashboard http://localhost:3000')}
|
||||
${chalk.green('npm run start:node -- --port 8082 --bootstrap localhost:8080 --dashboard http://localhost:3000')}
|
||||
|
||||
📈 Step 3: Monitor Your Network
|
||||
Open ${chalk.cyan('http://localhost:3000')} in your browser to see:
|
||||
|
||||
✅ Real-time Network Statistics
|
||||
- Total active nodes
|
||||
- Oracle nodes count
|
||||
- Total connections
|
||||
- Last update time
|
||||
|
||||
✅ Live Node List
|
||||
- Node status indicators
|
||||
- Node types (Oracle/Regular)
|
||||
- Ring positions
|
||||
- Connection health
|
||||
|
||||
✅ Interactive Ring Topology
|
||||
- Visual ring structure
|
||||
- Node positioning
|
||||
- Click nodes for details
|
||||
- Color-coded by type
|
||||
|
||||
✅ REST API Access
|
||||
- GET /api/nodes - List all nodes
|
||||
- GET /api/network/stats - Network statistics
|
||||
- GET /api/network/topology - Ring topology
|
||||
- GET /api/nodes/:id - Node details
|
||||
|
||||
📡 API Examples:
|
||||
${chalk.cyan('curl http://localhost:3000/api/network/stats')}
|
||||
${chalk.cyan('curl http://localhost:3000/api/nodes')}
|
||||
${chalk.cyan('curl http://localhost:3000/api/network/topology')}
|
||||
|
||||
🔄 Features:
|
||||
- Auto-refresh every 5 seconds
|
||||
- Inactive node detection (5 minute timeout)
|
||||
- Responsive design for mobile/desktop
|
||||
- Real-time topology updates
|
||||
- Connection health monitoring
|
||||
|
||||
The dashboard provides a comprehensive view of your RingNet
|
||||
network without affecting node performance!
|
||||
`));
|
||||
4
node.js
4
node.js
@@ -19,6 +19,9 @@ for (let i = 0; i < args.length; i++) {
|
||||
case '--bootstrap':
|
||||
options.bootstrap = args[++i];
|
||||
break;
|
||||
case '--dashboard':
|
||||
options.dashboardUrl = args[++i];
|
||||
break;
|
||||
case '--ice-servers':
|
||||
try {
|
||||
options.iceServers = JSON.parse(args[++i]);
|
||||
@@ -49,6 +52,7 @@ Options:
|
||||
--port <port> Port to listen on (default: random)
|
||||
--id <id> Node ID (default: auto-generated)
|
||||
--bootstrap <addr> Bootstrap node address (host:port)
|
||||
--dashboard <url> Dashboard server URL (default: http://localhost:3000)
|
||||
--ice-servers <json> ICE servers configuration (JSON array)
|
||||
--config <file> Load configuration from JSON file
|
||||
--help Show this help message
|
||||
|
||||
@@ -19,6 +19,9 @@ for (let i = 0; i < args.length; i++) {
|
||||
case '--bootstrap':
|
||||
options.bootstrap = args[++i];
|
||||
break;
|
||||
case '--dashboard':
|
||||
options.dashboardUrl = args[++i];
|
||||
break;
|
||||
case '--ice-servers':
|
||||
try {
|
||||
options.iceServers = JSON.parse(args[++i]);
|
||||
@@ -49,6 +52,7 @@ Options:
|
||||
--port <port> Port to listen on (default: random)
|
||||
--id <id> Node ID (default: auto-generated)
|
||||
--bootstrap <addr> Bootstrap node address (host:port)
|
||||
--dashboard <url> Dashboard server URL (default: http://localhost:3000)
|
||||
--ice-servers <json> ICE servers configuration (JSON array)
|
||||
--config <file> Load configuration from JSON file
|
||||
--help Show this help message
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
"start": "node index.js",
|
||||
"start:oracle": "node oracle.js",
|
||||
"start:node": "node node.js",
|
||||
"start:dashboard": "cd server && npm start",
|
||||
"dashboard:dev": "cd server && npm run dev",
|
||||
"demo": "node demo.js",
|
||||
"demo:dashboard": "node demo-dashboard.js",
|
||||
"example": "node examples/basic-example.js",
|
||||
"test": "node test/test-network.js"
|
||||
},
|
||||
@@ -18,6 +21,7 @@
|
||||
"@koush/wrtc": "^0.5.3",
|
||||
"chalk": "^5.3.0",
|
||||
"express": "^4.18.2",
|
||||
"node-fetch": "^3.3.2",
|
||||
"simple-peer": "^9.11.1",
|
||||
"uuid": "^9.0.1",
|
||||
"ws": "^8.14.2"
|
||||
|
||||
159
server/README.md
Archivo normal
159
server/README.md
Archivo normal
@@ -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
20
server/package.json
Archivo normal
@@ -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
414
server/public/index.html
Archivo normal
@@ -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
279
server/server.js
Archivo normal
@@ -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;
|
||||
@@ -34,6 +34,8 @@ export class RingNode extends EventEmitter {
|
||||
this.activeSignalingConnections = new Map(); // Store active WebSocket connections for signaling
|
||||
this.nodePositions = new Map(); // Track all node positions in the ring
|
||||
this.ringSize = 1000; // Virtual ring size for position calculation
|
||||
this.dashboardUrl = options.dashboardUrl || 'http://localhost:3000'; // Dashboard server URL
|
||||
this.dashboardReportInterval = null;
|
||||
|
||||
// Assign initial position for this node
|
||||
this.assignPosition(this.id);
|
||||
@@ -44,6 +46,7 @@ export class RingNode extends EventEmitter {
|
||||
// Start connection management
|
||||
this.startConnectionManager();
|
||||
this.startHeartbeat();
|
||||
this.startDashboardReporting();
|
||||
}
|
||||
|
||||
getRandomPort() {
|
||||
@@ -812,6 +815,12 @@ export class RingNode extends EventEmitter {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
}
|
||||
if (this.dashboardReportInterval) {
|
||||
clearInterval(this.dashboardReportInterval);
|
||||
}
|
||||
if (this.dashboardReportInterval) {
|
||||
clearInterval(this.dashboardReportInterval);
|
||||
}
|
||||
|
||||
// Notify peers of shutdown
|
||||
this.webrtc.broadcast({
|
||||
@@ -891,6 +900,59 @@ export class RingNode extends EventEmitter {
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
// Dashboard reporting methods
|
||||
startDashboardReporting() {
|
||||
// Initial registration
|
||||
this.reportToDashboard();
|
||||
|
||||
// Report every 30 seconds
|
||||
this.dashboardReportInterval = setInterval(() => {
|
||||
this.reportToDashboard();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
async reportToDashboard() {
|
||||
try {
|
||||
const nodeInfo = {
|
||||
nodeId: this.id,
|
||||
port: this.port,
|
||||
isOracle: this.isOracle,
|
||||
ringPosition: this.ringPosition,
|
||||
rings: this.rings,
|
||||
connectedPeers: this.webrtc.getConnectedPeers(),
|
||||
knownNodes: Array.from(this.knownNodes.keys()),
|
||||
oracleNodes: Array.from(this.oracleNodes),
|
||||
nodePositions: Object.fromEntries(this.nodePositions),
|
||||
status: 'active',
|
||||
connections: this.getPersistentConnections()
|
||||
};
|
||||
|
||||
// Use dynamic import to avoid adding fetch as a dependency
|
||||
const { default: fetch } = await import('node-fetch').catch(() => ({ default: null }));
|
||||
|
||||
if (!fetch) {
|
||||
// Fallback: just log that we would report
|
||||
console.log(chalk.blue(`📊 Would report to dashboard: ${this.id.substring(0, 8)}...`));
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.dashboardUrl}/api/nodes/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(nodeInfo),
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Successfully reported
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently handle dashboard reporting errors - dashboard might not be running
|
||||
}
|
||||
}
|
||||
|
||||
getPersistentConnections() {
|
||||
const persistentConnections = [];
|
||||
for (const [nodeId, nodeInfo] of this.knownNodes.entries()) {
|
||||
|
||||
Referencia en una nueva incidencia
Block a user