feat: consolidate utility functions into P2NS Plugin SDK

- Centralized common formatting, DOM, and status utilities in `includes/plugins/sdk.js`
- Created `sdk.utils.format`, `sdk.utils.dom`, and `sdk.utils.status` namespaces
- Refactored `domain.consensus` and `peer.visualize` plugins to use the global SDK
- Updated `plugin-handler` to serve SDK utilities globally via `/sdk-utils.js`
- Enhanced SDK validation by delegating to the core validation infrastructure
- Fixed bugs in metric rendering and missing frontend utility functions
This commit is contained in:
Raven Scott
2025-12-17 22:21:58 -05:00
parent 5f5ed3848e
commit ad4784d05d
12 changed files with 1090 additions and 373 deletions
+4 -11
View File
@@ -2,6 +2,7 @@ const { createBackup, listBackups, restoreBackup, cleanupOldBackups } = require(
const { logError } = require('../../../infrastructure/logger');
const { trackRequest, trackRequestWithTiming } = require('../../../maintenance/metrics');
const { createErrorResponse } = require('../../../infrastructure/error_handler');
const sdk = require('../../../plugins/sdk');
const fs = require('fs').promises;
const path = require('path');
@@ -20,7 +21,7 @@ async function handleBackupsRoutes(req, res) {
return {
...backup,
size: backup.size || 0,
sizeFormatted: formatBytes(backup.size || 0)
sizeFormatted: sdk.utils.format.formatBytes(backup.size || 0)
};
});
@@ -218,14 +219,14 @@ async function handleBackupsRoutes(req, res) {
return {
name: fileName,
size: stats.size,
sizeFormatted: formatBytes(stats.size),
sizeFormatted: sdk.utils.format.formatBytes(stats.size),
modified: stats.mtime.toISOString()
};
} catch (err) {
return {
name: fileName,
size: 0,
sizeFormatted: '0 B',
sizeFormatted: '0 Bytes',
modified: null
};
}
@@ -264,13 +265,5 @@ async function handleBackupsRoutes(req, res) {
return false;
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
module.exports = { handleBackupsRoutes };
@@ -565,10 +565,12 @@ async function handleSettingsRoutes(req, res) {
}));
// Trigger graceful shutdown after sending response
// Add a longer delay to ensure storage cleanup is complete and state is reset
setTimeout(() => {
logInfo('Admin', 'Initiating graceful shutdown after DNS storage cleanup...');
logInfo('Admin', '🔄 SYSTEM IS SHUTTING DOWN - Storage has been cleaned and will reinitialize on restart');
process.emit('SIGTERM');
}, 1000); // Give time for response to be sent
}, 3000); // Give more time for response to be sent and cleanup to complete
} catch (err) {
logError('Admin', `Failed to clean DNS storage: ${err.message}`);
res.writeHead(500, { 'Content-Type': 'application/json' });
+4 -4
View File
@@ -157,7 +157,7 @@ window.tabs = {
const li = document.createElement('li');
li.className = 'p-4 bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow';
const uptime = peer.uptime ? (window.formatUptime ? window.formatUptime(peer.uptime) : `${Math.floor(peer.uptime / 1000)}s`) : 'N/A';
const uptime = peer.uptime ? (window.sdk?.utils?.format?.formatUptime ? window.sdk.utils.format.formatUptime(peer.uptime) : `${Math.floor(peer.uptime / 1000)}s`) : 'N/A';
const statusBadge = peer.connected
? '<span class="px-2 py-1 bg-green-500 rounded text-sm" style="color: var(--text-primary);">Connected</span>'
: '<span class="px-2 py-1 bg-gray-500 rounded text-sm" style="color: var(--text-primary);">Disconnected</span>';
@@ -175,7 +175,7 @@ window.tabs = {
</div>
<div class="text-sm theme-text-secondary">
<div>Uptime: ${uptime}</div>
<div>Connections: ${peer.metrics?.connections || 0} | Avg Duration: ${peer.metrics?.avgDuration ? window.formatDuration ? window.formatDuration(peer.metrics.avgDuration) : `${Math.floor(peer.metrics.avgDuration / 1000)}s` : 'N/A'}</div>
<div>Connections: ${peer.metrics?.connections || 0} | Avg Duration: ${peer.metrics?.avgDuration ? (window.sdk?.utils?.format?.formatDuration ? window.sdk.utils.format.formatDuration(peer.metrics.avgDuration) : `${Math.floor(peer.metrics.avgDuration / 1000)}s`) : 'N/A'}</div>
</div>
</div>
<div class="flex gap-2 ml-4 flex-shrink-0">
@@ -333,7 +333,7 @@ window.tabs = {
: `<button onclick="deleteHolesailServer('${item.id}')" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Delete</button>`;
const protocol = item.opts.udp ? 'UDP' : 'TCP';
const url = item.info.url || 'N/A';
const truncatedUrl = window.truncateUrl ? window.truncateUrl(url, 40) : url.length > 40 ? url.substring(0, 37) + '...' : url;
const truncatedUrl = window.sdk?.utils?.dom?.truncate ? window.sdk.utils.dom.truncate(url, 40) : (url.length > 40 ? url.substring(0, 37) + '...' : url);
const tr = document.createElement('tr');
tr.className = 'border-b hover:bg-gray-50 dark:hover:bg-gray-700';
tr.innerHTML = `
@@ -370,7 +370,7 @@ window.tabs = {
: `<button onclick="deleteHolesailClient('${item.id}')" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Delete</button>`;
const protocol = (item.opts.protocol || 'tcp').toUpperCase();
const key = item.opts.key || '';
const truncatedKey = window.truncateUrl ? window.truncateUrl(key, 30) : key.length > 30 ? key.substring(0, 27) + '...' : key;
const truncatedKey = window.sdk?.utils?.dom?.truncate ? window.sdk.utils.dom.truncate(key, 30) : (key.length > 30 ? key.substring(0, 27) + '...' : key);
const tr = document.createElement('tr');
tr.className = 'border-b hover:bg-gray-50 dark:hover:bg-gray-700';
tr.innerHTML = `
+272 -77
View File
@@ -1,5 +1,209 @@
// Utility functions
function renderStatusBadge(state) {
/**
* P2NS Admin Panel - Utilities
* Consolidated utilities matching the P2NS Plugin SDK
*/
// Initialize sdk global if not present
window.sdk = window.sdk || {};
window.sdk.utils = window.sdk.utils || {};
/**
* Formatting Utilities
*/
window.sdk.utils.format = {
/**
* Format a timestamp to human-readable relative time
*/
formatTimestamp(timestamp) {
if (!timestamp) return 'Never';
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) {
return `${Math.floor(diff / 1000)}s ago`;
} else if (diff < 3600000) {
return `${Math.floor(diff / 60000)}m ago`;
} else if (diff < 86400000) {
return `${Math.floor(diff / 3600000)}h ago`;
} else {
return date.toLocaleString();
}
},
/**
* Format a peer ID to shortened version
*/
formatPeerId(peerId, short = true) {
if (!peerId) return 'N/A';
if (!short) return peerId;
if (peerId.length <= 16) return peerId;
return `${peerId.slice(0, 8)}...${peerId.slice(-8)}`;
},
/**
* Format a hash to shortened version
*/
formatHash(hash) {
if (!hash) return 'N/A';
if (hash.length <= 20) return hash;
return `${hash.slice(0, 10)}...${hash.slice(-10)}`;
},
/**
* Format duration (milliseconds to readable string)
*/
formatDuration(ms) {
if (!ms || ms === 0) return '-';
if (ms < 1000) return `${Math.round(ms)}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
if (ms < 3600000) return `${(ms / 60000).toFixed(2)}m`;
return `${(ms / 3600000).toFixed(2)}h`;
},
/**
* Format uptime (milliseconds to readable string)
*/
formatUptime(ms) {
if (!ms || ms === 0) return '0s';
const days = Math.floor(ms / 86400000);
const hours = Math.floor((ms % 86400000) / 3600000);
const minutes = Math.floor((ms % 3600000) / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
if (days > 0) return `${days}d ${hours}h ${minutes}m`;
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
if (minutes > 0) return `${minutes}m ${seconds}s`;
return `${seconds}s`;
},
/**
* Format bytes to human readable string
*/
formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
if (!bytes) return 'N/A';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
},
/**
* Format number with commas
*/
formatNumber(num) {
if (num === null || num === undefined) return '0';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
},
/**
* Format CPU usage
*/
formatCPUUsage(cpuUsage, uptime) {
if (!cpuUsage) return 'N/A';
if (typeof cpuUsage.percentage === 'number') {
return `${cpuUsage.percentage.toFixed(2)}%`;
}
if (typeof cpuUsage.user === 'number' && typeof cpuUsage.system === 'number') {
if (uptime && uptime > 0) {
const uptimeMicroseconds = uptime * 1000;
const totalCpuMicroseconds = cpuUsage.user + cpuUsage.system;
const cpuPercent = (totalCpuMicroseconds / uptimeMicroseconds) * 100;
return `${cpuPercent.toFixed(2)}%`;
}
return `${(cpuUsage.user / 1000).toFixed(2)}ms user, ${(cpuUsage.system / 1000).toFixed(2)}ms system`;
}
return 'N/A';
},
/**
* Format memory usage
*/
formatMemoryUsage(memoryUsage) {
if (!memoryUsage) return 'N/A';
const rssMB = (memoryUsage.rss / 1024 / 1024).toFixed(2);
const heapUsedMB = (memoryUsage.heapUsed / 1024 / 1024).toFixed(2);
const heapTotalMB = (memoryUsage.heapTotal / 1024 / 1024).toFixed(2);
return `${rssMB} MB RSS (${heapUsedMB}/${heapTotalMB} MB heap)`;
},
/**
* Format Holesail hash for display
*/
formatHolesailHash(hash) {
if (!hash) return 'none';
if (hash.startsWith('hs://')) return hash;
return `hs://${hash}`;
}
};
/**
* DOM and UI Utilities
*/
window.sdk.utils.dom = {
escapeHtml(text) {
if (typeof text !== 'string') return text;
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
async copyToClipboard(text) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
throw new Error('Clipboard API not available');
} catch (err) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
return successful;
}
},
truncate(text, maxLength = 40) {
if (!text || text === 'N/A') return 'N/A';
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + '...';
},
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
};
/**
* Status and Badge Utilities
*/
window.sdk.utils.status = {
renderStatusBadge(state) {
const badges = {
'running': { class: 'bg-green-500', icon: '✓', text: 'Running' },
'stopped': { class: 'bg-gray-500', icon: '○', text: 'Stopped' },
@@ -11,88 +215,79 @@ function renderStatusBadge(state) {
<span>${badge.icon}</span>
<span>${badge.text}</span>
</span>`;
},
getConsensusBadgeClass(status) {
switch (status) {
case 'resolved': return 'status-badge resolved';
case 'insufficient_quorum': return 'status-badge insufficient_quorum';
case 'tie': return 'status-badge tie';
case 'no_claims': return 'status-badge no_claims';
case 'error': return 'status-badge error';
default: return 'status-badge no_claims';
}
},
function truncateUrl(url, maxLength = 40) {
if (!url || url === 'N/A') return 'N/A';
if (url.length <= maxLength) return url;
return url.substring(0, maxLength - 3) + '...';
/**
* Get display text for a consensus status
*/
getConsensusText(status) {
switch (status) {
case 'resolved': return 'Resolved';
case 'insufficient_quorum': return 'Insufficient Quorum';
case 'tie': return 'Tie';
case 'no_claims': return 'No Claims';
case 'error': return 'Error';
default: return 'Unknown';
}
},
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
/**
* Get hex color for a consensus status
*/
getConsensusColor(status) {
switch (status) {
case 'resolved': return '#10b981';
case 'insufficient_quorum': return '#eab308';
case 'tie': return '#f97316';
case 'no_claims': return '#6b7280';
case 'error': return '#ef4444';
default: return '#6b7280';
}
// Formatting functions for stats
function formatUptime(ms) {
const days = Math.floor(ms / 86400000);
const hours = Math.floor((ms % 86400000) / 3600000);
const minutes = Math.floor((ms % 3600000) / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
if (days > 0) return `${days}d ${hours}h ${minutes}m`;
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
if (minutes > 0) return `${minutes}m ${seconds}s`;
return `${seconds}s`;
}
};
function formatDuration(ms) {
if (!ms || ms === 0) return '-';
if (ms < 1000) return `${Math.round(ms)}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
if (ms < 3600000) return `${(ms / 60000).toFixed(2)}m`;
return `${(ms / 3600000).toFixed(2)}h`;
}
/**
* Legacy compatibility layers
*/
window.renderStatusBadge = window.sdk.utils.status.renderStatusBadge;
window.truncateUrl = (url, maxLength) => window.sdk.utils.dom.truncate(url, maxLength);
window.escapeHtml = window.sdk.utils.dom.escapeHtml;
window.formatUptime = window.sdk.utils.format.formatUptime;
window.formatDuration = window.sdk.utils.format.formatDuration;
window.formatCPUUsage = window.sdk.utils.format.formatCPUUsage;
window.formatMemoryUsage = window.sdk.utils.format.formatMemoryUsage;
// Default chart colors if not defined
window.chartColors = window.chartColors || {
primary: 'rgb(59, 130, 246)',
success: 'rgb(34, 197, 94)',
warning: 'rgb(234, 179, 8)',
danger: 'rgb(239, 68, 68)',
info: 'rgb(59, 130, 246)',
gray: 'rgb(107, 114, 128)',
dark: 'rgb(17, 24, 39)'
};
function formatCPUUsage(cpuUsage, uptime) {
if (!cpuUsage) return 'N/A';
window.darkModeColors = window.darkModeColors || {
primary: 'rgb(96, 165, 250)',
success: 'rgb(74, 222, 128)',
warning: 'rgb(250, 204, 21)',
danger: 'rgb(248, 113, 113)',
info: 'rgb(96, 165, 250)',
gray: 'rgb(156, 163, 175)',
dark: 'rgb(243, 244, 246)'
};
// Handle pidusage format (has percentage property)
if (typeof cpuUsage.percentage === 'number') {
return `${cpuUsage.percentage.toFixed(2)}%`;
}
// Handle old format with user/system in microseconds
if (typeof cpuUsage.user === 'number' && typeof cpuUsage.system === 'number') {
// CPU usage is in microseconds, uptime is in milliseconds
// Calculate average CPU percentage over process lifetime
if (uptime && uptime > 0) {
const uptimeMicroseconds = uptime * 1000; // Convert ms to microseconds
const totalCpuMicroseconds = cpuUsage.user + cpuUsage.system;
const cpuPercent = (totalCpuMicroseconds / uptimeMicroseconds) * 100;
return `${cpuPercent.toFixed(2)}%`;
}
// Fallback: show raw values if no uptime
const userMs = (cpuUsage.user / 1000).toFixed(2);
const systemMs = (cpuUsage.system / 1000).toFixed(2);
return `${userMs}ms user, ${systemMs}ms system`;
}
return 'N/A';
}
function formatMemoryUsage(memoryUsage) {
if (!memoryUsage) return 'N/A';
// Memory usage is in bytes, convert to MB
const rssMB = (memoryUsage.rss / 1024 / 1024).toFixed(2);
const heapUsedMB = (memoryUsage.heapUsed / 1024 / 1024).toFixed(2);
const heapTotalMB = (memoryUsage.heapTotal / 1024 / 1024).toFixed(2);
return `${rssMB} MB RSS (${heapUsedMB}/${heapTotalMB} MB heap)`;
}
function getChartColors() {
window.getChartColors = () => {
return document.documentElement.classList.contains('dark') ? window.darkModeColors : window.chartColors;
}
// Make functions globally accessible
window.renderStatusBadge = renderStatusBadge;
window.truncateUrl = truncateUrl;
window.escapeHtml = escapeHtml;
window.formatUptime = formatUptime;
window.formatDuration = formatDuration;
window.formatCPUUsage = formatCPUUsage;
window.formatMemoryUsage = formatMemoryUsage;
window.getChartColors = getChartColors;
};
+275 -36
View File
@@ -1,5 +1,209 @@
// Utility functions
function renderStatusBadge(state) {
/**
* P2NS Admin Panel - Utilities
* Consolidated utilities matching the P2NS Plugin SDK
*/
// Initialize sdk global if not present
window.sdk = window.sdk || {};
window.sdk.utils = window.sdk.utils || {};
/**
* Formatting Utilities
*/
window.sdk.utils.format = {
/**
* Format a timestamp to human-readable relative time
*/
formatTimestamp(timestamp) {
if (!timestamp) return 'Never';
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) {
return `${Math.floor(diff / 1000)}s ago`;
} else if (diff < 3600000) {
return `${Math.floor(diff / 60000)}m ago`;
} else if (diff < 86400000) {
return `${Math.floor(diff / 3600000)}h ago`;
} else {
return date.toLocaleString();
}
},
/**
* Format a peer ID to shortened version
*/
formatPeerId(peerId, short = true) {
if (!peerId) return 'N/A';
if (!short) return peerId;
if (peerId.length <= 16) return peerId;
return `${peerId.slice(0, 8)}...${peerId.slice(-8)}`;
},
/**
* Format a hash to shortened version
*/
formatHash(hash) {
if (!hash) return 'N/A';
if (hash.length <= 20) return hash;
return `${hash.slice(0, 10)}...${hash.slice(-10)}`;
},
/**
* Format duration (milliseconds to readable string)
*/
formatDuration(ms) {
if (!ms || ms === 0) return '-';
if (ms < 1000) return `${Math.round(ms)}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
if (ms < 3600000) return `${(ms / 60000).toFixed(2)}m`;
return `${(ms / 3600000).toFixed(2)}h`;
},
/**
* Format uptime (milliseconds to readable string)
*/
formatUptime(ms) {
if (!ms || ms === 0) return '0s';
const days = Math.floor(ms / 86400000);
const hours = Math.floor((ms % 86400000) / 3600000);
const minutes = Math.floor((ms % 3600000) / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
if (days > 0) return `${days}d ${hours}h ${minutes}m`;
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
if (minutes > 0) return `${minutes}m ${seconds}s`;
return `${seconds}s`;
},
/**
* Format bytes to human readable string
*/
formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
if (!bytes) return 'N/A';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
},
/**
* Format number with commas
*/
formatNumber(num) {
if (num === null || num === undefined) return '0';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
},
/**
* Format CPU usage
*/
formatCPUUsage(cpuUsage, uptime) {
if (!cpuUsage) return 'N/A';
if (typeof cpuUsage.percentage === 'number') {
return `${cpuUsage.percentage.toFixed(2)}%`;
}
if (typeof cpuUsage.user === 'number' && typeof cpuUsage.system === 'number') {
if (uptime && uptime > 0) {
const uptimeMicroseconds = uptime * 1000;
const totalCpuMicroseconds = cpuUsage.user + cpuUsage.system;
const cpuPercent = (totalCpuMicroseconds / uptimeMicroseconds) * 100;
return `${cpuPercent.toFixed(2)}%`;
}
return `${(cpuUsage.user / 1000).toFixed(2)}ms user, ${(cpuUsage.system / 1000).toFixed(2)}ms system`;
}
return 'N/A';
},
/**
* Format memory usage
*/
formatMemoryUsage(memoryUsage) {
if (!memoryUsage) return 'N/A';
const rssMB = (memoryUsage.rss / 1024 / 1024).toFixed(2);
const heapUsedMB = (memoryUsage.heapUsed / 1024 / 1024).toFixed(2);
const heapTotalMB = (memoryUsage.heapTotal / 1024 / 1024).toFixed(2);
return `${rssMB} MB RSS (${heapUsedMB}/${heapTotalMB} MB heap)`;
},
/**
* Format Holesail hash for display
*/
formatHolesailHash(hash) {
if (!hash) return 'none';
if (hash.startsWith('hs://')) return hash;
return `hs://${hash}`;
}
};
/**
* DOM and UI Utilities
*/
window.sdk.utils.dom = {
escapeHtml(text) {
if (typeof text !== 'string') return text;
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
async copyToClipboard(text) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
throw new Error('Clipboard API not available');
} catch (err) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
return successful;
}
},
truncate(text, maxLength = 40) {
if (!text || text === 'N/A') return 'N/A';
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + '...';
},
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
};
/**
* Status and Badge Utilities
*/
window.sdk.utils.status = {
renderStatusBadge(state) {
const badges = {
'running': { class: 'bg-green-500', icon: '✓', text: 'Running' },
'stopped': { class: 'bg-gray-500', icon: '○', text: 'Stopped' },
@@ -7,49 +211,84 @@ function renderStatusBadge(state) {
'error': { class: 'bg-red-500', icon: '✗', text: 'Error' }
};
const badge = badges[state] || badges['stopped'];
return `<span class="px-2 py-1 ${badge.class} text-white text-xs font-semibold rounded-full flex items-center gap-1 w-fit" title="${badge.text}">
return `<span class="px-2 py-1 ${badge.class} text-xs font-semibold rounded-full flex items-center gap-1 w-fit" style="color: var(--text-primary);" title="${badge.text}">
<span>${badge.icon}</span>
<span>${badge.text}</span>
</span>`;
},
getConsensusBadgeClass(status) {
switch (status) {
case 'resolved': return 'status-badge resolved';
case 'insufficient_quorum': return 'status-badge insufficient_quorum';
case 'tie': return 'status-badge tie';
case 'no_claims': return 'status-badge no_claims';
case 'error': return 'status-badge error';
default: return 'status-badge no_claims';
}
},
function truncateUrl(url, maxLength = 40) {
if (!url || url === 'N/A') return 'N/A';
if (url.length <= maxLength) return url;
return url.substring(0, maxLength - 3) + '...';
/**
* Get display text for a consensus status
*/
getConsensusText(status) {
switch (status) {
case 'resolved': return 'Resolved';
case 'insufficient_quorum': return 'Insufficient Quorum';
case 'tie': return 'Tie';
case 'no_claims': return 'No Claims';
case 'error': return 'Error';
default: return 'Unknown';
}
},
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
/**
* Get hex color for a consensus status
*/
getConsensusColor(status) {
switch (status) {
case 'resolved': return '#10b981';
case 'insufficient_quorum': return '#eab308';
case 'tie': return '#f97316';
case 'no_claims': return '#6b7280';
case 'error': return '#ef4444';
default: return '#6b7280';
}
}
};
// Make functions globally accessible
window.renderStatusBadge = renderStatusBadge;
window.truncateUrl = truncateUrl;
window.escapeHtml = escapeHtml;
/**
* Legacy compatibility layers
*/
window.renderStatusBadge = window.sdk.utils.status.renderStatusBadge;
window.truncateUrl = (url, maxLength) => window.sdk.utils.dom.truncate(url, maxLength);
window.escapeHtml = window.sdk.utils.dom.escapeHtml;
window.formatUptime = window.sdk.utils.format.formatUptime;
window.formatDuration = window.sdk.utils.format.formatDuration;
window.formatCPUUsage = window.sdk.utils.format.formatCPUUsage;
window.formatMemoryUsage = window.sdk.utils.format.formatMemoryUsage;
// Default chart colors if not defined
window.chartColors = window.chartColors || {
primary: 'rgb(59, 130, 246)',
success: 'rgb(34, 197, 94)',
warning: 'rgb(234, 179, 8)',
danger: 'rgb(239, 68, 68)',
info: 'rgb(59, 130, 246)',
gray: 'rgb(107, 114, 128)',
dark: 'rgb(17, 24, 39)'
};
window.darkModeColors = window.darkModeColors || {
primary: 'rgb(96, 165, 250)',
success: 'rgb(74, 222, 128)',
warning: 'rgb(250, 204, 21)',
danger: 'rgb(248, 113, 113)',
info: 'rgb(96, 165, 250)',
gray: 'rgb(156, 163, 175)',
dark: 'rgb(243, 244, 246)'
};
window.getChartColors = () => {
return document.documentElement.classList.contains('dark') ? window.darkModeColors : window.chartColors;
};
+22
View File
@@ -625,6 +625,28 @@ async function handlePluginRequest(domain, req, res) {
}
}
// Serve sdk-utils.js globally to all plugins
if (urlPath === '/sdk-utils.js' || urlPath.endsWith('/sdk-utils.js')) {
try {
const sdkUtilsPath = path.join(__dirname, '..', 'admin', 'admin-frontend', 'utils.js');
const sdkUtilsContent = await fs.readFile(sdkUtilsPath, 'utf8');
res.writeHead(200, {
'Content-Type': 'application/javascript',
'Cache-Control': 'public, max-age=3600'
});
res.end(sdkUtilsContent);
return true;
} catch (err) {
logError('PluginHandler', `Error serving sdk-utils.js: ${err.message}`);
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Error loading SDK utilities');
}
return true;
}
}
if (urlPath === '/api/token' || urlPath.endsWith('/api/token')) {
// Set SDK plugin context (needed for SDK to work)
sdk._initializePluginContext(domain, path.join(process.cwd(), 'plugin-sites', domain), {});
+374 -57
View File
@@ -39,6 +39,7 @@ const { createBackup, listBackups, restoreBackup } = require('../maintenance/bac
const { validateConfig } = require('../infrastructure/config');
const { loadSubscriptions, saveSubscriptions, subscribeToService, unsubscribeFromService } = require('../admin/subscription-manager');
const { ensurePortFree } = require('../admin/admin-backend/port-management');
const validation = require('../infrastructure/validation');
const crypto = require('crypto');
const WebSocket = require('ws');
const EventEmitter = require('events');
@@ -2825,6 +2826,348 @@ const sdk = {
* Helper functions for common operations
*/
utils: {
/**
* Formatting Utilities
*/
format: {
/**
* Format a timestamp to human-readable relative time
* @param {number|Date|string} timestamp - Timestamp to format
* @returns {string} Formatted string (e.g., "5m ago")
*/
formatTimestamp(timestamp) {
if (!timestamp) return 'Never';
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) {
return `${Math.floor(diff / 1000)}s ago`;
} else if (diff < 3600000) {
return `${Math.floor(diff / 60000)}m ago`;
} else if (diff < 86400000) {
return `${Math.floor(diff / 3600000)}h ago`;
} else {
return date.toLocaleString();
}
},
/**
* Format a peer ID to shortened version
* @param {string} peerId - Peer ID to format
* @param {boolean} short - Whether to use extra short version (default: true)
* @returns {string} Formatted peer ID
*/
formatPeerId(peerId, short = true) {
if (!peerId) return 'N/A';
if (!short) return peerId;
if (peerId.length <= 16) return peerId;
return `${peerId.slice(0, 8)}...${peerId.slice(-8)}`;
},
/**
* Format a hash to shortened version
* @param {string} hash - Hash to format
* @returns {string} Formatted hash
*/
formatHash(hash) {
if (!hash) return 'N/A';
if (hash.length <= 20) return hash;
return `${hash.slice(0, 10)}...${hash.slice(-10)}`;
},
/**
* Format duration (milliseconds to readable string)
* @param {number} ms - Milliseconds
* @returns {string} Formatted duration
*/
formatDuration(ms) {
if (!ms || ms === 0) return '0s';
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days}d ${hours % 24}h`;
} else if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
} else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
} else {
return `${seconds}s`;
}
},
/**
* Format uptime (milliseconds to readable string with days/hours/minutes)
* @param {number} ms - Milliseconds
* @returns {string} Formatted uptime
*/
formatUptime(ms) {
if (!ms || ms === 0) return '0s';
const days = Math.floor(ms / 86400000);
const hours = Math.floor((ms % 86400000) / 3600000);
const minutes = Math.floor((ms % 3600000) / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
if (days > 0) return `${days}d ${hours}h ${minutes}m`;
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
if (minutes > 0) return `${minutes}m ${seconds}s`;
return `${seconds}s`;
},
/**
* Format bytes to human readable string (KB, MB, GB)
* @param {number} bytes - Number of bytes
* @returns {string} Formatted bytes
*/
formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
if (!bytes) return 'N/A';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
},
/**
* Format number with comma separators
* @param {number} num - Number to format
* @returns {string} Formatted number
*/
formatNumber(num) {
if (num === null || num === undefined) return '0';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
},
/**
* Format CPU usage object to percentage string
* @param {Object} cpuUsage - CPU usage object
* @param {number} uptime - Process uptime in ms (optional)
* @returns {string} Formatted percentage
*/
formatCPUUsage(cpuUsage, uptime) {
if (!cpuUsage) return 'N/A';
// Handle pidusage format (has percentage property)
if (typeof cpuUsage.percentage === 'number') {
return `${cpuUsage.percentage.toFixed(2)}%`;
}
// Handle old format with user/system in microseconds
if (typeof cpuUsage.user === 'number' && typeof cpuUsage.system === 'number') {
if (uptime && uptime > 0) {
const uptimeMicroseconds = uptime * 1000;
const totalCpuMicroseconds = cpuUsage.user + cpuUsage.system;
const cpuPercent = (totalCpuMicroseconds / uptimeMicroseconds) * 100;
return `${cpuPercent.toFixed(2)}%`;
}
const userMs = (cpuUsage.user / 1000).toFixed(2);
const systemMs = (cpuUsage.system / 1000).toFixed(2);
return `${userMs}ms user, ${systemMs}ms system`;
}
return 'N/A';
},
/**
* Format memory usage object to MB string
* @param {Object} memoryUsage - Memory usage object (from process.memoryUsage())
* @returns {string} Formatted memory string
*/
formatMemoryUsage(memoryUsage) {
if (!memoryUsage) return 'N/A';
const rssMB = (memoryUsage.rss / 1024 / 1024).toFixed(2);
const heapUsedMB = (memoryUsage.heapUsed / 1024 / 1024).toFixed(2);
const heapTotalMB = (memoryUsage.heapTotal / 1024 / 1024).toFixed(2);
return `${rssMB} MB RSS (${heapUsedMB}/${heapTotalMB} MB heap)`;
},
/**
* Format Holesail hash for display
* @param {string} hash - Holesail hash
* @returns {string} Formatted hash
*/
formatHolesailHash(hash) {
if (!hash) return 'none';
if (hash.startsWith('hs://')) return hash;
return `hs://${hash}`;
}
},
/**
* DOM and UI Utilities (some functions browser-only)
*/
dom: {
/**
* Escape HTML to prevent XSS
* @param {string} text - Text to escape
* @returns {string} Escaped HTML
*/
escapeHtml(text) {
if (typeof text !== 'string') return text;
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
},
/**
* Copy text to clipboard (browser-only)
* @param {string} text - Text to copy
* @returns {Promise<boolean>} Success status
*/
async copyToClipboard(text) {
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
return false;
}
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
throw new Error('Clipboard API not available');
} catch (err) {
try {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
return successful;
} catch (err2) {
return false;
}
}
},
/**
* Truncate a URL or string for display
* @param {string} text - Text to truncate
* @param {number} maxLength - Maximum length (default: 40)
* @returns {string} Truncated text
*/
truncate(text, maxLength = 40) {
if (!text || text === 'N/A') return 'N/A';
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + '...';
},
/**
* Debounce function calls
* @param {Function} func - Function to debounce
* @param {number} wait - Wait time in ms
* @returns {Function} Debounced function
*/
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
/**
* Throttle function calls
* @param {Function} func - Function to throttle
* @param {number} limit - Limit time in ms
* @returns {Function} Throttled function
*/
throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
},
/**
* Status and Badge Utilities
*/
status: {
/**
* Render a status badge HTML string
* @param {string} state - Status state (running, stopped, starting, error)
* @returns {string} HTML string for the badge
*/
renderStatusBadge(state) {
const badges = {
'running': { class: 'bg-green-500', icon: '✓', text: 'Running' },
'stopped': { class: 'bg-gray-500', icon: '○', text: 'Stopped' },
'starting': { class: 'bg-yellow-500', icon: '⟳', text: 'Starting' },
'error': { class: 'bg-red-500', icon: '✗', text: 'Error' }
};
const badge = badges[state] || badges['stopped'];
return `<span class="px-2 py-1 ${badge.class} text-white text-xs font-semibold rounded-full flex items-center gap-1 w-fit" title="${badge.text}">
<span>${badge.icon}</span>
<span>${badge.text}</span>
</span>`;
},
/**
* Get CSS class for a consensus status
* @param {string} status - Consensus status
* @returns {string} CSS class
*/
getConsensusBadgeClass(status) {
switch (status) {
case 'resolved': return 'status-badge resolved';
case 'insufficient_quorum': return 'status-badge insufficient_quorum';
case 'tie': return 'status-badge tie';
case 'no_claims': return 'status-badge no_claims';
case 'error': return 'status-badge error';
default: return 'status-badge no_claims';
}
},
/**
* Get display text for a consensus status
* @param {string} status - Consensus status
* @returns {string} Status text
*/
getConsensusText(status) {
switch (status) {
case 'resolved': return 'Resolved';
case 'insufficient_quorum': return 'Insufficient Quorum';
case 'tie': return 'Tie';
case 'no_claims': return 'No Claims';
case 'error': return 'Error';
default: return 'Unknown';
}
},
/**
* Get hex color for a consensus status
* @param {string} status - Consensus status
* @returns {string} Hex color
*/
getConsensusColor(status) {
switch (status) {
case 'resolved': return '#10b981';
case 'insufficient_quorum': return '#eab308';
case 'tie': return '#f97316';
case 'no_claims': return '#6b7280';
case 'error': return '#ef4444';
default: return '#6b7280';
}
}
},
/**
* Check if DNS service is initialized
* @returns {boolean} True if initialized
@@ -2919,37 +3262,6 @@ const sdk = {
throw lastError;
},
/**
* Debounce function calls
* @param {Function} fn - Function to debounce
* @param {number} delay - Delay in milliseconds
* @returns {Function} Debounced function
*/
debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
},
/**
* Throttle function calls
* @param {Function} fn - Function to throttle
* @param {number} delay - Delay in milliseconds
* @returns {Function} Throttled function
*/
throttle(fn, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
return fn.apply(this, args);
}
};
},
/**
* Sleep/delay utility
* @param {number} ms - Milliseconds to sleep
@@ -2959,17 +3271,6 @@ const sdk = {
return new Promise(resolve => setTimeout(resolve, ms));
},
/**
* Format Holesail hash for display
* @param {string} hash - Holesail hash
* @returns {string} Formatted hash
*/
formatHash(hash) {
if (!hash) return 'none';
if (hash.startsWith('hs://')) return hash;
return `hs://${hash}`;
},
/**
* Parse Holesail hash
* @param {string} hash - Holesail hash
@@ -3080,15 +3381,12 @@ const sdk = {
*/
security: {
/**
* Validate domain name format
* Validate domain name format (supports punycode and localhost)
* @param {string} domain - Domain name
* @returns {boolean} True if valid
*/
validateDomain(domain) {
if (!domain || typeof domain !== 'string') return false;
// Basic domain validation
const domainRegex = /^([a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i;
return domainRegex.test(domain) && domain.length <= 253;
return validation.validateDomain(domain);
},
/**
@@ -3097,13 +3395,34 @@ const sdk = {
* @returns {boolean} True if valid
*/
validateHash(hash) {
if (!hash || typeof hash !== 'string') return false;
// Holesail hashes start with hs:// or are base32 encoded
if (hash.startsWith('hs://')) {
return hash.length > 5;
}
// Basic validation for base32-like strings
return /^[a-z2-7]+$/i.test(hash) && hash.length > 10;
return validation.validateHolesailHash(hash);
},
/**
* Validate a port number
* @param {number|string} port - Port to validate
* @returns {boolean} True if valid
*/
validatePort(port) {
return validation.validatePort(port);
},
/**
* Validate an IP address
* @param {string} ip - IP address to validate
* @returns {boolean} True if valid
*/
validateIP(ip) {
return validation.validateIP(ip);
},
/**
* Validate a file path (basic validation to prevent directory traversal)
* @param {string} path - Path to validate
* @returns {boolean} True if valid
*/
validateFilePath(path) {
return validation.validateFilePath(path);
},
/**
@@ -3112,9 +3431,7 @@ const sdk = {
* @returns {string} Sanitized string
*/
sanitizeInput(input) {
if (typeof input !== 'string') return '';
// Remove potentially dangerous characters
return input.replace(/[<>\"'&]/g, '');
return validation.sanitizeInput(input);
},
/**
+88 -12
View File
@@ -322,6 +322,7 @@ async function main() {
}
let dnsPass = null;
let core = null;
let isShuttingDown = false;
// Helper function to safely get dnsPass and ensure state consistency
function getDnsPass() {
@@ -916,7 +917,7 @@ async function main() {
logInfo('Swarm', `Starting persistent invite retry loop (interval: ${retryIntervalMs}ms, relay after ${relayAfterAttempts} attempts, max attempts: ${maxRetryAttempts})`);
persistentInviteRetryInterval = setInterval(() => {
const pass = state.dnsPass || dnsPass;
const pass = getDnsPass();
if (pass) {
// Got invite, stop the loop
logInfo('Swarm', 'dnsPass initialized, stopping persistent invite retry loop');
@@ -1144,7 +1145,11 @@ async function main() {
}
if (config.CONSENSUS_IMMEDIATE_UPDATE !== false) {
try {
await doAutoVotes();
} catch (err) {
logError('Swarm', `Error in immediate auto-votes: ${err.message}`);
}
} else {
// Debounced auto-votes for batch operations
if (listenerRefs.autoVoteDebounce) {
@@ -1182,7 +1187,11 @@ async function main() {
}
if (config.CONSENSUS_IMMEDIATE_UPDATE !== false) {
try {
await doAutoVotes();
} catch (err) {
logError('Swarm', `Error in immediate auto-votes: ${err.message}`);
}
}
await listDomains();
@@ -1215,7 +1224,11 @@ async function main() {
}
if (config.CONSENSUS_IMMEDIATE_UPDATE !== false) {
try {
await doAutoVotes();
} catch (err) {
logError('Swarm', `Error in immediate auto-votes: ${err.message}`);
}
}
await listDomains();
@@ -1254,6 +1267,9 @@ async function main() {
let swarmKeepAliveTimer = null;
if (swarmKeepAliveInterval > 0) {
swarmKeepAliveTimer = setInterval(() => {
// Skip if we're shutting down
if (isShuttingDown) return;
try {
// Rejoin the topic (idempotent operation - safe to call multiple times)
// This ensures the swarm maintains its connection to the DHT network
@@ -1271,6 +1287,16 @@ async function main() {
// Set up connection handler with timeout handling for better local network peer support
swarm.on('connection', async (conn, info) => {
// Skip processing if we're shutting down
if (isShuttingDown) {
logDebug('Swarm', 'Ignoring new connection during shutdown');
try {
conn.destroy();
} catch (err) {
// Ignore errors when destroying during shutdown
}
return;
}
// Set socket timeout to prevent hanging connections (helps with local network peers)
if (conn && conn.socket) {
const socketTimeout = parseInt(process.env.SWARM_CONNECTION_TIMEOUT || '30000', 10); // 30 seconds default
@@ -1617,7 +1643,7 @@ async function main() {
// Function to broadcast invite request to all connected peers
async function broadcastInviteRequest() {
const pass = state.dnsPass || dnsPass;
const pass = getDnsPass();
if (pass) {
logDebug('Swarm', 'Already have dnsPass, no need to broadcast invite request');
return;
@@ -1731,6 +1757,11 @@ async function main() {
connectedPeers.delete(peerId);
peerChannels.delete(peerId);
// Clean up peer tracking data structures to prevent memory leaks
failedInvitePeers.delete(peerId);
pendingInviteAcks.delete(peerId);
trackPeerEvent('disconnect', peerId);
broadcast({ type: 'update-peers' });
broadcast({ type: 'update-stats' });
@@ -2226,12 +2257,16 @@ async function main() {
logInfo('Main', 'Resource validation started');
logInfo('Main', 'Process will continue running. Press Ctrl+C to exit.');
let isShuttingDown = false;
// Enhanced cleanup on exit
const cleanup = async () => {
if (isShuttingDown) return;
isShuttingDown = true;
logInfo('Main', 'Shutting down gracefully...');
// Start periodic "system is shutting down" messages every 3 seconds
let shutdownMessageInterval = setInterval(() => {
logInfo('Main', '🔄 SYSTEM IS SHUTTING DOWN - Please wait for graceful cleanup to complete...');
}, 3000);
try {
// Remove swarm event listeners
swarm.removeAllListeners('connection');
@@ -2337,6 +2372,16 @@ async function main() {
logDebug('Main', 'Cleared swarm keep-alive timer');
}
logDebug('Main', 'Removed event listeners from dnsPass and core');
// Remove swarm event listeners to prevent memory leaks
logDebug('Main', 'Removing swarm event listeners...');
try {
swarm.removeAllListeners('connection');
swarm.removeAllListeners('update');
swarm.removeAllListeners('error');
logDebug('Main', 'Removed swarm event listeners');
} catch (err) {
logError('Main', `Error removing swarm event listeners: ${err.message}`);
}
// Leave swarm topics
logDebug('Main', 'Leaving swarm topics...');
try {
@@ -2345,7 +2390,14 @@ async function main() {
} catch (err) {
logError('Main', `Error leaving topic: ${err.message}`);
}
// Close swarm connections
// Immediately destroy swarm to prevent new connections during cleanup
logDebug('Main', 'Destroying swarm to prevent new connections...');
try {
await swarm.destroy();
} catch (err) {
logError('Main', `Error destroying swarm: ${err.message}`);
}
// Close any remaining swarm connections
logDebug('Main', 'Closing swarm connections...');
const connections = Array.from(swarm.connections);
for (const conn of connections) {
@@ -2358,31 +2410,41 @@ async function main() {
logError('Main', `Error destroying connection: ${err.message}`);
}
}
// Close autopass
// Close autopass (skip if already cleaned/reset during shutdown)
if (state.dnsPass && typeof state.dnsPass.close === 'function') {
logDebug('Main', 'Closing dnsPass...');
try {
await state.dnsPass.close();
} catch (err) {
// Ignore errors if dnsPass was already cleaned/reset
if (err.message.includes('Autobase failed to open') ||
err.message.includes('Corestore is closed') ||
err.message.includes('already closed')) {
logDebug('Main', 'dnsPass already cleaned/closed, skipping cleanup');
} else {
logError('Main', `Error closing dnsPass: ${err.message}`);
}
}
// Destroy swarm
logDebug('Main', 'Destroying swarm...');
try {
await swarm.destroy();
} catch (err) {
logError('Main', `Error destroying swarm: ${err.message}`);
} else {
logDebug('Main', 'dnsPass not available or already closed');
}
// Close corestore
// Close corestore (skip if already cleaned/reset during shutdown)
if (store && typeof store.close === 'function') {
logDebug('Main', 'Closing corestore...');
try {
await store.close();
} catch (err) {
// Ignore errors if corestore was already cleaned/reset
if (err.message.includes('Corestore is closed') ||
err.message.includes('already closed')) {
logDebug('Main', 'Corestore already cleaned/closed, skipping cleanup');
} else {
logError('Main', `Error closing corestore: ${err.message}`);
}
}
} else {
logDebug('Main', 'Corestore not available or already closed');
}
// Close Holesail servers and clients
for (const [id, child] of state.holesailChildren) {
try {
@@ -2560,6 +2622,15 @@ async function main() {
if (domainsDebounceTimer) {
clearTimeout(domainsDebounceTimer);
}
// Remove stdin event listeners to prevent memory leaks
logDebug('Main', 'Removing stdin event listeners...');
try {
process.stdin.removeAllListeners('data');
process.stdin.pause();
logDebug('Main', 'Removed stdin event listeners');
} catch (err) {
logError('Main', `Error removing stdin event listeners: ${err.message}`);
}
// Restore console methods
const { restoreConsoleMethods } = require('./includes/admin');
logDebug('Main', 'Restoring console methods...');
@@ -2585,6 +2656,11 @@ async function main() {
} catch (err) {
logError('Main', `Error closing WebSocket connections: ${err.message}`);
}
// Clear shutdown message interval
if (shutdownMessageInterval) {
clearInterval(shutdownMessageInterval);
shutdownMessageInterval = null;
}
logInfo('Main', 'Cleanup completed successfully');
} catch (err) {
logError('Main', `Error during cleanup: ${err.message}`);
@@ -176,6 +176,7 @@
</div>
<!-- Scripts -->
<script src="/sdk-utils.js"></script>
<script src="/js/utils.js"></script>
<script src="/js/api.js"></script>
<script src="/js/websocket.js"></script>
+16 -96
View File
@@ -1,103 +1,48 @@
/**
* Utility functions for domain consensus plugin
* Now using P2NS Plugin SDK for common utilities
*/
/**
* Format a timestamp to human-readable string
*/
function formatTimestamp(timestamp) {
if (!timestamp) return 'Never';
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) {
return `${Math.floor(diff / 1000)}s ago`;
} else if (diff < 3600000) {
return `${Math.floor(diff / 60000)}m ago`;
} else if (diff < 86400000) {
return `${Math.floor(diff / 3600000)}h ago`;
} else {
return date.toLocaleString();
}
return window.sdk?.utils?.format?.formatTimestamp(timestamp) || 'Never';
}
/**
* Format a peer ID to shortened version
*/
function formatPeerId(peerId) {
if (!peerId) return 'N/A';
if (peerId.length <= 16) return peerId;
return `${peerId.slice(0, 8)}...${peerId.slice(-8)}`;
return window.sdk?.utils?.format?.formatPeerId(peerId) || 'N/A';
}
/**
* Format a hash to shortened version
*/
function formatHash(hash) {
if (!hash) return 'N/A';
if (hash.length <= 20) return hash;
return `${hash.slice(0, 10)}...${hash.slice(-10)}`;
return window.sdk?.utils?.format?.formatHash(hash) || 'N/A';
}
/**
* Get status badge class based on consensus status
*/
function getStatusBadgeClass(status) {
switch (status) {
case 'resolved':
return 'status-badge resolved';
case 'insufficient_quorum':
return 'status-badge insufficient_quorum';
case 'tie':
return 'status-badge tie';
case 'no_claims':
return 'status-badge no_claims';
case 'error':
return 'status-badge error';
default:
return 'status-badge no_claims';
}
return window.sdk?.utils?.status?.getConsensusBadgeClass(status) || 'status-badge no_claims';
}
/**
* Get status text for consensus status
*/
function getStatusText(status) {
switch (status) {
case 'resolved':
return 'Resolved';
case 'insufficient_quorum':
return 'Insufficient Quorum';
case 'tie':
return 'Tie';
case 'no_claims':
return 'No Claims';
case 'error':
return 'Error';
default:
return 'Unknown';
}
return window.sdk?.utils?.status?.getConsensusText(status) || 'Unknown';
}
/**
* Get status color for consensus status
*/
function getStatusColor(status) {
switch (status) {
case 'resolved':
return '#10b981'; // green-500
case 'insufficient_quorum':
return '#eab308'; // yellow-500
case 'tie':
return '#f97316'; // orange-500
case 'no_claims':
return '#6b7280'; // gray-500
case 'error':
return '#ef4444'; // red-500
default:
return '#6b7280'; // gray-500
}
return window.sdk?.utils?.status?.getConsensusColor(status) || '#6b7280';
}
/**
@@ -112,58 +57,34 @@ function calculateQuorumPercentage(totalVotes, minVotes) {
* Escape HTML to prevent XSS
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
return window.sdk?.utils?.dom?.escapeHtml(text) || text;
}
/**
* Copy text to clipboard
*/
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
document.body.removeChild(textArea);
return true;
} catch (err) {
document.body.removeChild(textArea);
if (window.sdk?.utils?.dom?.copyToClipboard) {
return await window.sdk.utils.dom.copyToClipboard(text);
}
return false;
}
}
}
/**
* Debounce function
*/
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
if (window.sdk?.utils?.dom?.debounce) {
return window.sdk.utils.dom.debounce(func, wait);
}
return func;
}
/**
* Format number with commas
*/
function formatNumber(num) {
if (num === null || num === undefined) return '0';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return window.sdk?.utils?.format?.formatNumber(num) || '0';
}
/**
@@ -229,4 +150,3 @@ window.utils = {
createProgressBar,
updateSidebarStats
};
@@ -261,6 +261,7 @@
</div>
<!-- Scripts -->
<script src="/sdk-utils.js"></script>
<script src="/js/utils.js"></script>
<script src="/js/data-processor.js"></script>
<script src="/js/websocket-client.js"></script>
+12 -61
View File
@@ -1,60 +1,27 @@
/**
* Utility functions for peer.visualize
* Now using P2NS Plugin SDK for common utilities
*/
/**
* Format peer ID (short or long)
*/
function formatPeerId(peerId, short = true) {
if (!peerId) return 'Unknown';
if (short) {
return peerId.slice(0, 16) + '...';
}
return peerId;
return window.sdk?.utils?.format?.formatPeerId(peerId, short) || 'Unknown';
}
/**
* Format timestamp to readable string
*/
function formatTimestamp(timestamp) {
if (!timestamp) return 'Never';
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) {
return 'Just now';
} else if (diff < 3600000) {
const minutes = Math.floor(diff / 60000);
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
} else if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
} else {
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
}
return window.sdk?.utils?.format?.formatTimestamp(timestamp) || 'Never';
}
/**
* Format duration (milliseconds to readable string)
*/
function formatDuration(ms) {
if (!ms || ms === 0) return '0s';
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days}d ${hours % 24}h`;
} else if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
} else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
} else {
return `${seconds}s`;
}
return window.sdk?.utils?.format?.formatDuration(ms) || '0s';
}
/**
@@ -79,29 +46,20 @@ function getNodeColor(node) {
* Debounce function
*/
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
if (window.sdk?.utils?.dom?.debounce) {
return window.sdk.utils.dom.debounce(func, wait);
}
return func;
}
/**
* Throttle function
*/
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
if (window.sdk?.utils?.dom?.throttle) {
return window.sdk.utils.dom.throttle(func, limit);
}
};
return func;
}
/**
@@ -137,11 +95,7 @@ function arraysEqual(a, b) {
* Format bytes to human readable
*/
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
return window.sdk?.utils?.format?.formatBytes(bytes) || '0 Bytes';
}
// Export for use in other modules
@@ -160,6 +114,3 @@ if (typeof module !== 'undefined' && module.exports) {
formatBytes
};
}