Files
p2ns/plugin-sites/peer.visualize/index.js
T
2026-05-30 21:01:31 -04:00

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
};