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:
@@ -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' });
|
||||
|
||||
@@ -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 = `
|
||||
|
||||
@@ -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
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
@@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
},
|
||||
|
||||
/**
|
||||
* 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);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user