Files
ringnet/server/public/index.html
2025-06-19 01:33:59 +02:00

485 líneas
14 KiB
HTML

<!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);
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: white;
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
}
.header-links {
display: flex;
gap: 1rem;
}
.repo-link, .docs-link, .domain-link {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 6px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
font-size: 0.9rem;
}
.repo-link:hover, .docs-link:hover, .domain-link:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.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);
}
.header {
flex-direction: column;
gap: 1rem;
text-align: center;
}
.header-links {
justify-content: center;
}
}
.footer {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 1rem 2rem;
border-top: 1px solid rgba(255, 255, 255, 0.2);
text-align: center;
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
}
.footer a {
color: rgba(255, 255, 255, 0.9);
text-decoration: none;
}
.footer a:hover {
color: white;
text-decoration: underline;
}
</style>
</head>
<body>
<div class="header">
<h1>🔗 RingNet Dashboard</h1>
<div class="header-links">
<a href="https://git.manalejandro.com/ale/ringnet" target="_blank" class="repo-link">📂 Repository</a>
<a href="https://pad.manalejandro.com/p/2ringsnet" target="_blank" class="docs-link">📝 Docs</a>
<a href="https://ringnet.cloud" target="_blank" class="domain-link">🌐 ringnet.cloud</a>
</div>
</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>
<div class="footer">
<p>
RingNet - Decentralized P2P Network |
<a href="https://git.manalejandro.com/ale/ringnet" target="_blank">Source Code</a> |
<a href="https://pad.manalejandro.com/p/2ringsnet" target="_blank">Documentation</a> |
<a href="https://ringnet.cloud" target="_blank">ringnet.cloud</a>
</p>
</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>