487 lines
14 KiB
JavaScript
487 lines
14 KiB
JavaScript
/**
|
|
* Peer Visualize Plugin
|
|
*
|
|
* Provides interactive, real-time visualization of all peer connections
|
|
* and the P2NS system architecture.
|
|
*/
|
|
|
|
const sdk = require('../../includes/plugins/sdk');
|
|
|
|
// Shared state
|
|
let updateInterval = null;
|
|
|
|
/**
|
|
* Enrich peers with profile data
|
|
* Fetches profiles for all peer IDs in parallel and adds profile data to each peer object
|
|
* @param {Array<object>} peers - Array of peer objects with id property
|
|
* @returns {Promise<Array<object>>} Array of peer objects with profile data added
|
|
*/
|
|
async function enrichPeersWithProfiles(peers) {
|
|
if (!peers || peers.length === 0) {
|
|
return peers;
|
|
}
|
|
|
|
// Extract unique peer IDs
|
|
const peerIds = [...new Set(peers.map(p => p.id).filter(Boolean))];
|
|
|
|
if (peerIds.length === 0) {
|
|
return peers;
|
|
}
|
|
|
|
// Fetch all profiles in parallel
|
|
const profilePromises = peerIds.map(async (peerId) => {
|
|
try {
|
|
const profile = await sdk.profiles.getProfile(peerId);
|
|
return { peerId, profile };
|
|
} catch (err) {
|
|
// Profile doesn't exist or error fetching - return null
|
|
sdk.log.debug('peer.visualize', `No profile found for peer ${peerId.slice(0, 16)}...`);
|
|
return { peerId, profile: null };
|
|
}
|
|
});
|
|
|
|
const results = await Promise.all(profilePromises);
|
|
const profileMap = new Map();
|
|
|
|
// Build the map
|
|
for (const { peerId, profile } of results) {
|
|
profileMap.set(peerId, profile);
|
|
}
|
|
|
|
// Add profile data to each peer object
|
|
return peers.map(peer => ({
|
|
...peer,
|
|
profile: profileMap.get(peer.id) || null
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Get complete system state
|
|
*/
|
|
async function getSystemState() {
|
|
const connectedPeers = sdk.state.peerIds;
|
|
const peerMetrics = sdk.state.peerMetrics;
|
|
const peerHistory = sdk.state.peerHistory;
|
|
const localPeerId = sdk.state.localPeerId;
|
|
const domains = await sdk.domains.listDomains();
|
|
const peerChannels = sdk.state.getPeerChannelSnapshot();
|
|
const systemMetrics = sdk.metrics.getSystemMetrics();
|
|
|
|
// Build peer details
|
|
const peers = connectedPeers.map(peerId => {
|
|
const peerInfo = sdk.peers.getPeerInfo(peerId);
|
|
if (peerInfo) {
|
|
return {
|
|
id: peerId,
|
|
connected: peerInfo.connected,
|
|
isLocal: peerId === localPeerId,
|
|
connectTime: peerInfo.connectTime,
|
|
uptime: peerInfo.uptime,
|
|
metrics: peerInfo.metrics,
|
|
history: peerInfo.history
|
|
};
|
|
}
|
|
// Fallback if peerInfo not available
|
|
const metrics = peerMetrics.get(peerId) || {};
|
|
const history = (peerHistory.get(peerId) || []).slice(-50);
|
|
return {
|
|
id: peerId,
|
|
connected: true,
|
|
isLocal: peerId === localPeerId,
|
|
connectTime: null,
|
|
uptime: 0,
|
|
metrics,
|
|
history
|
|
};
|
|
});
|
|
|
|
// Add disconnected peers from history
|
|
const allPeerIds = new Set(connectedPeers);
|
|
for (const [peerId, history] of peerHistory.entries()) {
|
|
if (!allPeerIds.has(peerId) && history.length > 0) {
|
|
const metrics = peerMetrics.get(peerId) || {};
|
|
allPeerIds.add(peerId);
|
|
peers.push({
|
|
id: peerId,
|
|
connected: false,
|
|
connectTime: null,
|
|
uptime: 0,
|
|
metrics,
|
|
history: history.slice(-50)
|
|
});
|
|
}
|
|
}
|
|
|
|
// Enrich peers with profile data
|
|
const peersWithProfiles = await enrichPeersWithProfiles(peers);
|
|
|
|
return {
|
|
peers: peersWithProfiles,
|
|
domains,
|
|
localPeerId,
|
|
peerChannels,
|
|
metrics: systemMetrics,
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get network topology data for graph rendering
|
|
*/
|
|
async function getTopology() {
|
|
const connectedPeers = sdk.state.peerIds;
|
|
const localPeerId = sdk.state.localPeerId;
|
|
const domains = await sdk.domains.listDomains();
|
|
const peerChannels = sdk.state.getPeerChannelSnapshot();
|
|
|
|
// Build nodes
|
|
const nodes = [];
|
|
const edges = [];
|
|
|
|
// Fetch profiles for all peer IDs
|
|
const allPeerIds = [localPeerId, ...connectedPeers].filter(Boolean);
|
|
const profileMap = new Map();
|
|
|
|
if (allPeerIds.length > 0) {
|
|
const profilePromises = allPeerIds.map(async (peerId) => {
|
|
try {
|
|
const profile = await sdk.profiles.getProfile(peerId);
|
|
return { peerId, profile };
|
|
} catch (err) {
|
|
return { peerId, profile: null };
|
|
}
|
|
});
|
|
|
|
const results = await Promise.all(profilePromises);
|
|
for (const { peerId, profile } of results) {
|
|
profileMap.set(peerId, profile);
|
|
}
|
|
}
|
|
|
|
// Add local node
|
|
if (localPeerId) {
|
|
const localProfile = profileMap.get(localPeerId);
|
|
nodes.push({
|
|
id: localPeerId,
|
|
type: 'peer',
|
|
label: localProfile?.displayName || 'Local Node',
|
|
isLocal: true,
|
|
connected: true,
|
|
profile: localProfile
|
|
});
|
|
}
|
|
|
|
// Add connected peers
|
|
for (const peerId of connectedPeers) {
|
|
if (peerId !== localPeerId) {
|
|
const peerProfile = profileMap.get(peerId);
|
|
nodes.push({
|
|
id: peerId,
|
|
type: 'peer',
|
|
label: peerProfile?.displayName || peerId.slice(0, 16) + '...',
|
|
isLocal: false,
|
|
connected: true,
|
|
profile: peerProfile
|
|
});
|
|
|
|
// Add edge to local node
|
|
if (localPeerId) {
|
|
edges.push({
|
|
source: localPeerId,
|
|
target: peerId,
|
|
type: 'connection',
|
|
bidirectional: true
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add domains
|
|
for (const domain of domains) {
|
|
nodes.push({
|
|
id: domain.domain,
|
|
type: 'domain',
|
|
label: domain.domain,
|
|
hash: domain.hash,
|
|
isLocal: domain.consensus?.resolvedClaimant === localPeerId
|
|
});
|
|
|
|
// Connect domain to its owner
|
|
if (domain.consensus?.resolvedClaimant) {
|
|
edges.push({
|
|
source: domain.consensus.resolvedClaimant,
|
|
target: domain.domain,
|
|
type: 'ownership'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Peer channel snapshot lists connected peers (edges to local node)
|
|
for (const entry of peerChannels) {
|
|
if (entry.connected && entry.peerId !== localPeerId) {
|
|
edges.push({
|
|
source: localPeerId || 'local',
|
|
target: entry.peerId,
|
|
type: 'p2p',
|
|
protocol: 'swarm'
|
|
});
|
|
}
|
|
}
|
|
|
|
return { nodes, edges };
|
|
}
|
|
|
|
/**
|
|
* Broadcast update to all WebSocket clients
|
|
*/
|
|
function broadcastUpdate(type, data) {
|
|
sdk.websocket.broadcast({ type, data, timestamp: Date.now() });
|
|
}
|
|
|
|
/**
|
|
* Setup periodic updates
|
|
*/
|
|
function setupPeriodicUpdates() {
|
|
// Clear existing interval
|
|
if (updateInterval) {
|
|
clearInterval(updateInterval);
|
|
}
|
|
|
|
// Send updates every 5 seconds
|
|
updateInterval = setInterval(async () => {
|
|
try {
|
|
const systemState = await getSystemState();
|
|
broadcastUpdate('system-update', systemState);
|
|
|
|
const topology = await getTopology();
|
|
broadcastUpdate('topology-update', topology);
|
|
|
|
const metrics = sdk.metrics.getSystemMetrics();
|
|
broadcastUpdate('metrics-update', metrics);
|
|
} catch (err) {
|
|
sdk.log.error('peer.visualize', `Error in periodic update: ${err.message}`);
|
|
}
|
|
}, 5000);
|
|
}
|
|
|
|
/**
|
|
* Plugin Handler
|
|
*/
|
|
async function handler(req, res) {
|
|
try {
|
|
const { path, method } = sdk.router.parseRequest(req);
|
|
|
|
// Handle root path - serve index.html
|
|
if (path === '' || path === '/') {
|
|
return false;
|
|
}
|
|
|
|
// GET /api/system - Get complete system state
|
|
if (path === 'api/system' && method === 'GET') {
|
|
const systemState = await getSystemState();
|
|
return sdk.router.json(res, systemState);
|
|
}
|
|
|
|
// GET /api/peers - Get detailed peer information
|
|
if (path === 'api/peers' && method === 'GET') {
|
|
const connectedPeers = sdk.state.peerIds;
|
|
const peerMetrics = sdk.state.peerMetrics;
|
|
const peerHistory = sdk.state.peerHistory;
|
|
const localPeerId = sdk.state.localPeerId;
|
|
|
|
const peers = connectedPeers.map(peerId => {
|
|
const metrics = peerMetrics.get(peerId) || {};
|
|
const history = (peerHistory.get(peerId) || []).slice(-50);
|
|
const connectTime = sdk.state.peerStartTimes?.get(peerId);
|
|
const uptime = connectTime ? Date.now() - connectTime : 0;
|
|
|
|
return {
|
|
id: peerId,
|
|
connected: true,
|
|
isLocal: peerId === localPeerId,
|
|
connectTime,
|
|
uptime,
|
|
metrics,
|
|
history
|
|
};
|
|
});
|
|
|
|
// Enrich peers with profile data
|
|
const peersWithProfiles = await enrichPeersWithProfiles(peers);
|
|
|
|
return sdk.router.json(res, { peers: peersWithProfiles, timestamp: Date.now() });
|
|
}
|
|
|
|
// GET /api/domains - Get domain information
|
|
if (path === 'api/domains' && method === 'GET') {
|
|
const domains = await sdk.domains.listDomains();
|
|
return sdk.router.json(res, { domains, timestamp: Date.now() });
|
|
}
|
|
|
|
// GET /api/topology - Get network topology data
|
|
if (path === 'api/topology' && method === 'GET') {
|
|
const topology = await getTopology();
|
|
return sdk.router.json(res, topology);
|
|
}
|
|
|
|
// GET /api/metrics - Get system-wide metrics
|
|
if (path === 'api/metrics' && method === 'GET') {
|
|
const metrics = sdk.metrics.getSystemMetrics();
|
|
return sdk.router.json(res, { metrics, timestamp: Date.now() });
|
|
}
|
|
|
|
// GET /api/peer/:peerId - Get detailed information for a specific peer
|
|
if (path.startsWith('api/peer/') && method === 'GET') {
|
|
const peerId = path.slice(9); // Remove 'api/peer/' prefix
|
|
const peerInfo = sdk.peers.getPeerInfo(peerId);
|
|
|
|
if (!peerInfo) {
|
|
return sdk.router.error(res, 'Peer not found', 404);
|
|
}
|
|
|
|
return sdk.router.json(res, peerInfo);
|
|
}
|
|
|
|
// GET /api/domain/:domain - Get detailed information for a specific domain
|
|
if (path.startsWith('api/domain/') && method === 'GET') {
|
|
const domain = path.slice(11); // Remove 'api/domain/' prefix
|
|
const domainInfo = await sdk.domains.getDomainInfo(domain);
|
|
|
|
if (!domainInfo || domainInfo.error) {
|
|
return sdk.router.error(res, 'Domain not found', 404);
|
|
}
|
|
|
|
return sdk.router.json(res, domainInfo);
|
|
}
|
|
|
|
// Return false for all other routes to allow static file serving
|
|
return false;
|
|
} catch (err) {
|
|
sdk.log.error('peer.visualize', `Error handling request: ${err.message}`);
|
|
return sdk.router.error(res, 'Internal Server Error', 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup WebSocket handlers using SDK
|
|
*/
|
|
function setupWebSocketHandlers() {
|
|
sdk.log.info('peer.visualize', 'Setting up WebSocket handlers...');
|
|
sdk.log.info('peer.visualize', `PLUGIN_DOMAIN is: ${process.env.PLUGIN_DOMAIN || 'NOT SET'}`);
|
|
|
|
// Explicitly initialize WebSocket server to ensure it's ready
|
|
const initialized = sdk.websocket.initialize();
|
|
sdk.log.info('peer.visualize', `WebSocket initialization result: ${initialized}`);
|
|
if (!initialized) {
|
|
sdk.log.error('peer.visualize', 'Failed to initialize WebSocket server');
|
|
return;
|
|
}
|
|
|
|
// Handle new connections
|
|
sdk.websocket.on('connection', (ws) => {
|
|
sdk.log.info('peer.visualize', `WebSocket client connected (${sdk.websocket.getClientCount()} total)`);
|
|
|
|
// Send initial state
|
|
getSystemState().then(systemState => {
|
|
sdk.websocket.send(ws, {
|
|
type: 'init',
|
|
data: systemState,
|
|
timestamp: Date.now()
|
|
});
|
|
}).catch(err => {
|
|
sdk.log.error('peer.visualize', `Error sending initial state: ${err.message}`);
|
|
});
|
|
});
|
|
|
|
// Handle incoming messages (auto-parsed JSON)
|
|
sdk.websocket.on('message', async (ws, message) => {
|
|
try {
|
|
if (message.type === 'request-system') {
|
|
const systemState = await getSystemState();
|
|
sdk.websocket.send(ws, {
|
|
type: 'system',
|
|
data: systemState,
|
|
timestamp: Date.now()
|
|
});
|
|
} else if (message.type === 'request-topology') {
|
|
const topology = await getTopology();
|
|
sdk.websocket.send(ws, {
|
|
type: 'topology',
|
|
data: topology,
|
|
timestamp: Date.now()
|
|
});
|
|
} else if (message.type === 'request-metrics') {
|
|
const metrics = sdk.metrics.getSystemMetrics();
|
|
sdk.websocket.send(ws, {
|
|
type: 'metrics',
|
|
data: metrics,
|
|
timestamp: Date.now()
|
|
});
|
|
}
|
|
} catch (err) {
|
|
sdk.log.error('peer.visualize', `Error handling WebSocket message: ${err.message}`);
|
|
}
|
|
});
|
|
|
|
// Handle client disconnections
|
|
sdk.websocket.on('close', (ws) => {
|
|
sdk.log.info('peer.visualize', `WebSocket client disconnected (${sdk.websocket.getClientCount()} total)`);
|
|
});
|
|
|
|
sdk.log.info('peer.visualize', 'WebSocket handlers registered');
|
|
}
|
|
|
|
/**
|
|
* Plugin Initialization Hook
|
|
*/
|
|
async function onInit() {
|
|
sdk.log.info('peer.visualize', 'Initializing plugin...');
|
|
|
|
try {
|
|
// Setup WebSocket handlers (server is automatically created and registered)
|
|
setupWebSocketHandlers();
|
|
|
|
// Setup periodic updates
|
|
setupPeriodicUpdates();
|
|
|
|
sdk.log.info('peer.visualize', 'Plugin initialized successfully');
|
|
} catch (err) {
|
|
sdk.log.error('peer.visualize', `Error during initialization: ${err.message}`);
|
|
if (err.stack) {
|
|
sdk.log.error('peer.visualize', `Stack trace: ${err.stack}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Plugin Shutdown Hook
|
|
*/
|
|
async function onShutdown() {
|
|
sdk.log.info('peer.visualize', 'Shutting down plugin...');
|
|
|
|
try {
|
|
// Clear update interval
|
|
if (updateInterval) {
|
|
clearInterval(updateInterval);
|
|
updateInterval = null;
|
|
}
|
|
|
|
// Close all WebSocket connections (SDK handles cleanup automatically)
|
|
sdk.websocket.close();
|
|
|
|
sdk.log.info('peer.visualize', 'Plugin shutdown complete');
|
|
} catch (err) {
|
|
sdk.log.error('peer.visualize', `Error during shutdown: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// Export the plugin interface
|
|
module.exports = {
|
|
handler,
|
|
onInit,
|
|
onShutdown
|
|
};
|
|
|
|
|
|
|