mirror of
https://github.com/yggdrasil-network/yggdrasil-go.git
synced 2025-08-24 16:05:07 +03:00
Enhance WebUI with new peer display features and multilingual support
This commit is contained in:
parent
83bd279ffa
commit
791214c18b
7 changed files with 679 additions and 168 deletions
|
@ -172,8 +172,94 @@ class YggdrasilUtils {
|
||||||
* @returns {string} - Status text
|
* @returns {string} - Status text
|
||||||
*/
|
*/
|
||||||
static getPeerStatusText(up) {
|
static getPeerStatusText(up) {
|
||||||
|
const currentLang = window.getCurrentLanguage ? window.getCurrentLanguage() : 'en';
|
||||||
|
if (window.translations && window.translations[currentLang]) {
|
||||||
|
return up
|
||||||
|
? window.translations[currentLang]['peer_status_online'] || 'Online'
|
||||||
|
: window.translations[currentLang]['peer_status_offline'] || 'Offline';
|
||||||
|
}
|
||||||
return up ? 'Online' : 'Offline';
|
return up ? 'Online' : 'Offline';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format latency to human readable format
|
||||||
|
* @param {number} latency - Latency in nanoseconds
|
||||||
|
* @returns {string} - Formatted latency
|
||||||
|
*/
|
||||||
|
static formatLatency(latency) {
|
||||||
|
if (!latency || latency === 0) return 'N/A';
|
||||||
|
const ms = latency / 1000000;
|
||||||
|
if (ms < 1) return `${(latency / 1000).toFixed(0)}μs`;
|
||||||
|
if (ms < 1000) return `${ms.toFixed(1)}ms`;
|
||||||
|
return `${(ms / 1000).toFixed(2)}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get direction text and icon
|
||||||
|
* @param {boolean} inbound - Whether connection is inbound
|
||||||
|
* @returns {Object} - Object with text and icon
|
||||||
|
*/
|
||||||
|
static getConnectionDirection(inbound) {
|
||||||
|
const currentLang = window.getCurrentLanguage ? window.getCurrentLanguage() : 'en';
|
||||||
|
let text;
|
||||||
|
if (window.translations && window.translations[currentLang]) {
|
||||||
|
text = inbound
|
||||||
|
? window.translations[currentLang]['peer_direction_inbound'] || 'Inbound'
|
||||||
|
: window.translations[currentLang]['peer_direction_outbound'] || 'Outbound';
|
||||||
|
} else {
|
||||||
|
text = inbound ? 'Inbound' : 'Outbound';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: text,
|
||||||
|
icon: inbound ? '↓' : '↑'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format port number for display
|
||||||
|
* @param {number} port - Port number
|
||||||
|
* @returns {string} - Formatted port
|
||||||
|
*/
|
||||||
|
static formatPort(port) {
|
||||||
|
return port ? `Port ${port}` : 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get quality indicator based on cost
|
||||||
|
* @param {number} cost - Connection cost
|
||||||
|
* @returns {Object} - Object with class and text
|
||||||
|
*/
|
||||||
|
static getQualityIndicator(cost) {
|
||||||
|
const currentLang = window.getCurrentLanguage ? window.getCurrentLanguage() : 'en';
|
||||||
|
let text;
|
||||||
|
if (window.translations && window.translations[currentLang]) {
|
||||||
|
if (!cost || cost === 0) {
|
||||||
|
text = window.translations[currentLang]['peer_quality_unknown'] || 'Unknown';
|
||||||
|
return { class: 'quality-unknown', text: text };
|
||||||
|
}
|
||||||
|
if (cost <= 100) {
|
||||||
|
text = window.translations[currentLang]['peer_quality_excellent'] || 'Excellent';
|
||||||
|
return { class: 'quality-excellent', text: text };
|
||||||
|
}
|
||||||
|
if (cost <= 200) {
|
||||||
|
text = window.translations[currentLang]['peer_quality_good'] || 'Good';
|
||||||
|
return { class: 'quality-good', text: text };
|
||||||
|
}
|
||||||
|
if (cost <= 400) {
|
||||||
|
text = window.translations[currentLang]['peer_quality_fair'] || 'Fair';
|
||||||
|
return { class: 'quality-fair', text: text };
|
||||||
|
}
|
||||||
|
text = window.translations[currentLang]['peer_quality_poor'] || 'Poor';
|
||||||
|
return { class: 'quality-poor', text: text };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to English
|
||||||
|
if (!cost || cost === 0) return { class: 'quality-unknown', text: 'Unknown' };
|
||||||
|
if (cost <= 100) return { class: 'quality-excellent', text: 'Excellent' };
|
||||||
|
if (cost <= 200) return { class: 'quality-good', text: 'Good' };
|
||||||
|
if (cost <= 400) return { class: 'quality-fair', text: 'Fair' };
|
||||||
|
return { class: 'quality-poor', text: 'Poor' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create global API instance
|
// Create global API instance
|
||||||
|
|
|
@ -54,8 +54,10 @@ function updateNodeInfoDisplay(info) {
|
||||||
// Update footer version
|
// Update footer version
|
||||||
updateElementText('footer-version', info.build_version || 'unknown');
|
updateElementText('footer-version', info.build_version || 'unknown');
|
||||||
|
|
||||||
// Update full key display (for copy functionality)
|
// Update full values for copy functionality
|
||||||
updateElementData('node-key-full', info.key || '');
|
updateElementData('node-key-full', info.key || '');
|
||||||
|
updateElementData('node-address', info.address || '');
|
||||||
|
updateElementData('node-subnet', info.subnet || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -75,7 +77,11 @@ function updatePeersDisplay(data) {
|
||||||
updateElementText('peers-online', onlineCount.toString());
|
updateElementText('peers-online', onlineCount.toString());
|
||||||
|
|
||||||
if (!data.peers || data.peers.length === 0) {
|
if (!data.peers || data.peers.length === 0) {
|
||||||
peersContainer.innerHTML = '<div class="no-data">No peers connected</div>';
|
const currentLang = window.getCurrentLanguage ? window.getCurrentLanguage() : 'en';
|
||||||
|
const message = window.translations && window.translations[currentLang]
|
||||||
|
? window.translations[currentLang]['no_peers_connected'] || 'No peers connected'
|
||||||
|
: 'No peers connected';
|
||||||
|
peersContainer.innerHTML = `<div class="no-data">${message}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,12 +89,24 @@ function updatePeersDisplay(data) {
|
||||||
const peerElement = createPeerElement(peer);
|
const peerElement = createPeerElement(peer);
|
||||||
peersContainer.appendChild(peerElement);
|
peersContainer.appendChild(peerElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update translations for newly added peer elements
|
||||||
|
if (typeof updateTexts === 'function') {
|
||||||
|
updateTexts();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose update functions to window for access from other scripts
|
// Expose update functions to window for access from other scripts
|
||||||
window.updateNodeInfoDisplay = updateNodeInfoDisplay;
|
window.updateNodeInfoDisplay = updateNodeInfoDisplay;
|
||||||
window.updatePeersDisplay = updatePeersDisplay;
|
window.updatePeersDisplay = updatePeersDisplay;
|
||||||
|
|
||||||
|
// Expose copy functions to window for access from HTML onclick handlers
|
||||||
|
window.copyNodeKey = copyNodeKey;
|
||||||
|
window.copyNodeAddress = copyNodeAddress;
|
||||||
|
window.copyNodeSubnet = copyNodeSubnet;
|
||||||
|
window.copyPeerAddress = copyPeerAddress;
|
||||||
|
window.copyPeerKey = copyPeerKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create HTML element for a single peer
|
* Create HTML element for a single peer
|
||||||
*/
|
*/
|
||||||
|
@ -98,21 +116,101 @@ function createPeerElement(peer) {
|
||||||
|
|
||||||
const statusClass = yggUtils.getPeerStatusClass(peer.up);
|
const statusClass = yggUtils.getPeerStatusClass(peer.up);
|
||||||
const statusText = yggUtils.getPeerStatusText(peer.up);
|
const statusText = yggUtils.getPeerStatusText(peer.up);
|
||||||
|
const direction = yggUtils.getConnectionDirection(peer.inbound);
|
||||||
|
const quality = yggUtils.getQualityIndicator(peer.cost);
|
||||||
|
const uptimeText = peer.uptime ? yggUtils.formatDuration(peer.uptime) : 'N/A';
|
||||||
|
const latencyText = yggUtils.formatLatency(peer.latency);
|
||||||
|
|
||||||
|
// Get translations for labels
|
||||||
|
const currentLang = window.getCurrentLanguage ? window.getCurrentLanguage() : 'en';
|
||||||
|
const t = window.translations && window.translations[currentLang] ? window.translations[currentLang] : {};
|
||||||
|
|
||||||
|
const labels = {
|
||||||
|
connection: t['peer_connection'] || 'Connection',
|
||||||
|
performance: t['peer_performance'] || 'Performance',
|
||||||
|
traffic: t['peer_traffic'] || 'Traffic',
|
||||||
|
uptime: t['peer_uptime'] || 'Uptime',
|
||||||
|
port: t['peer_port'] || 'Port',
|
||||||
|
priority: t['peer_priority'] || 'Priority',
|
||||||
|
latency: t['peer_latency'] || 'Latency',
|
||||||
|
cost: t['peer_cost'] || 'Cost',
|
||||||
|
quality: t['peer_quality'] || 'Quality',
|
||||||
|
received: t['peer_received'] || '↓ Received',
|
||||||
|
sent: t['peer_sent'] || '↑ Sent',
|
||||||
|
total: t['peer_total'] || 'Total',
|
||||||
|
remove: t['peer_remove'] || 'Remove'
|
||||||
|
};
|
||||||
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<div class="peer-header">
|
<div class="peer-header">
|
||||||
<div class="peer-address">${peer.address || 'N/A'}</div>
|
<div class="peer-address-section">
|
||||||
<div class="peer-status ${statusClass}">${statusText}</div>
|
<div class="peer-address copyable" onclick="copyPeerAddress('${peer.address || ''}')" data-key-title="copy_address_tooltip">${peer.address || 'N/A'}</div>
|
||||||
|
<div class="peer-key copyable" onclick="copyPeerKey('${peer.key || ''}')" data-key-title="copy_key_tooltip">${yggUtils.formatPublicKey(peer.key) || 'N/A'}</div>
|
||||||
|
</div>
|
||||||
|
<div class="peer-status-section">
|
||||||
|
<div class="peer-status ${statusClass}">${statusText}</div>
|
||||||
|
<div class="peer-direction ${peer.inbound ? 'inbound' : 'outbound'}" title="${direction.text}">
|
||||||
|
${direction.icon} ${direction.text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="peer-details">
|
<div class="peer-details">
|
||||||
<div class="peer-uri" title="${peer.remote || 'N/A'}">${peer.remote || 'N/A'}</div>
|
<div class="peer-uri" title="${peer.remote || 'N/A'}">${peer.remote || 'N/A'}</div>
|
||||||
<div class="peer-stats">
|
<div class="peer-info-grid">
|
||||||
<span>↓ ${yggUtils.formatBytes(peer.bytes_recvd || 0)}</span>
|
<div class="peer-info-section">
|
||||||
<span>↑ ${yggUtils.formatBytes(peer.bytes_sent || 0)}</span>
|
<div class="peer-info-title">${labels.connection}</div>
|
||||||
${peer.up && peer.latency ? `<span>RTT: ${(peer.latency / 1000000).toFixed(1)}ms</span>` : ''}
|
<div class="peer-info-stats">
|
||||||
|
<span class="info-item">
|
||||||
|
<span class="info-label">${labels.uptime}:</span>
|
||||||
|
<span class="info-value">${uptimeText}</span>
|
||||||
|
</span>
|
||||||
|
<span class="info-item">
|
||||||
|
<span class="info-label">${labels.port}:</span>
|
||||||
|
<span class="info-value">${peer.port || 'N/A'}</span>
|
||||||
|
</span>
|
||||||
|
<span class="info-item">
|
||||||
|
<span class="info-label">${labels.priority}:</span>
|
||||||
|
<span class="info-value">${peer.priority !== undefined ? peer.priority : 'N/A'}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="peer-info-section">
|
||||||
|
<div class="peer-info-title">${labels.performance}</div>
|
||||||
|
<div class="peer-info-stats">
|
||||||
|
<span class="info-item">
|
||||||
|
<span class="info-label">${labels.latency}:</span>
|
||||||
|
<span class="info-value">${latencyText}</span>
|
||||||
|
</span>
|
||||||
|
<span class="info-item">
|
||||||
|
<span class="info-label">${labels.cost}:</span>
|
||||||
|
<span class="info-value">${peer.cost !== undefined ? peer.cost : 'N/A'}</span>
|
||||||
|
</span>
|
||||||
|
<span class="info-item quality-indicator">
|
||||||
|
<span class="info-label">${labels.quality}:</span>
|
||||||
|
<span class="info-value ${quality.class}">${quality.text}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="peer-info-section">
|
||||||
|
<div class="peer-info-title">${labels.traffic}</div>
|
||||||
|
<div class="peer-info-stats">
|
||||||
|
<span class="info-item">
|
||||||
|
<span class="info-label">${labels.received}:</span>
|
||||||
|
<span class="info-value">${yggUtils.formatBytes(peer.bytes_recvd || 0)}</span>
|
||||||
|
</span>
|
||||||
|
<span class="info-item">
|
||||||
|
<span class="info-label">${labels.sent}:</span>
|
||||||
|
<span class="info-value">${yggUtils.formatBytes(peer.bytes_sent || 0)}</span>
|
||||||
|
</span>
|
||||||
|
<span class="info-item">
|
||||||
|
<span class="info-label">${labels.total}:</span>
|
||||||
|
<span class="info-value">${yggUtils.formatBytes((peer.bytes_recvd || 0) + (peer.bytes_sent || 0))}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${peer.remote ? `<button class="peer-remove-btn" onclick="removePeerConfirm('${peer.remote}')">Remove</button>` : ''}
|
${peer.remote ? `<button class="peer-remove-btn" onclick="removePeerConfirm('${peer.remote}')">${labels.remove}</button>` : ''}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return div;
|
return div;
|
||||||
|
@ -193,7 +291,11 @@ function updateElementData(id, data) {
|
||||||
async function copyToClipboard(text) {
|
async function copyToClipboard(text) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
showSuccess('Copied to clipboard');
|
const currentLang = window.getCurrentLanguage ? window.getCurrentLanguage() : 'en';
|
||||||
|
const message = window.translations && window.translations[currentLang]
|
||||||
|
? window.translations[currentLang]['copied_to_clipboard'] || 'Copied to clipboard'
|
||||||
|
: 'Copied to clipboard';
|
||||||
|
showSuccess(message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to copy:', error);
|
console.error('Failed to copy:', error);
|
||||||
showError('Failed to copy to clipboard');
|
showError('Failed to copy to clipboard');
|
||||||
|
@ -213,6 +315,50 @@ function copyNodeKey() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy node address to clipboard
|
||||||
|
*/
|
||||||
|
function copyNodeAddress() {
|
||||||
|
const element = document.getElementById('node-address');
|
||||||
|
if (element) {
|
||||||
|
const address = element.getAttribute('data-value') || element.textContent;
|
||||||
|
if (address && address !== 'N/A' && address !== 'Загрузка...') {
|
||||||
|
copyToClipboard(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy node subnet to clipboard
|
||||||
|
*/
|
||||||
|
function copyNodeSubnet() {
|
||||||
|
const element = document.getElementById('node-subnet');
|
||||||
|
if (element) {
|
||||||
|
const subnet = element.getAttribute('data-value') || element.textContent;
|
||||||
|
if (subnet && subnet !== 'N/A' && subnet !== 'Загрузка...') {
|
||||||
|
copyToClipboard(subnet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy peer address to clipboard
|
||||||
|
*/
|
||||||
|
function copyPeerAddress(address) {
|
||||||
|
if (address && address !== 'N/A') {
|
||||||
|
copyToClipboard(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy peer key to clipboard
|
||||||
|
*/
|
||||||
|
function copyPeerKey(key) {
|
||||||
|
if (key && key !== 'N/A') {
|
||||||
|
copyToClipboard(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-refresh data
|
* Auto-refresh data
|
||||||
*/
|
*/
|
||||||
|
@ -250,7 +396,11 @@ async function initializeApp() {
|
||||||
// Load initial data
|
// Load initial data
|
||||||
await Promise.all([loadNodeInfo(), loadPeers()]);
|
await Promise.all([loadNodeInfo(), loadPeers()]);
|
||||||
|
|
||||||
showSuccess('Dashboard loaded successfully');
|
const currentLang = window.getCurrentLanguage ? window.getCurrentLanguage() : 'en';
|
||||||
|
const message = window.translations && window.translations[currentLang]
|
||||||
|
? window.translations[currentLang]['dashboard_loaded'] || 'Dashboard loaded successfully'
|
||||||
|
: 'Dashboard loaded successfully';
|
||||||
|
showSuccess(message);
|
||||||
|
|
||||||
// Start auto-refresh
|
// Start auto-refresh
|
||||||
startAutoRefresh();
|
startAutoRefresh();
|
||||||
|
|
|
@ -34,106 +34,93 @@
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="layout">
|
<aside class="sidebar">
|
||||||
<aside class="sidebar">
|
<nav class="nav-menu">
|
||||||
<nav class="nav-menu">
|
<div class="nav-item active" onclick="showSection('status')">
|
||||||
<div class="nav-item active" onclick="showSection('status')">
|
<span class="nav-icon">📊</span>
|
||||||
<span class="nav-icon">📊</span>
|
<span class="nav-text" data-key="nav_status">Состояние</span>
|
||||||
<span class="nav-text" data-key="nav_status">Состояние</span>
|
</div>
|
||||||
</div>
|
<div class="nav-item" onclick="showSection('peers')">
|
||||||
<div class="nav-item" onclick="showSection('peers')">
|
<span class="nav-icon">🌐</span>
|
||||||
<span class="nav-icon">🌐</span>
|
<span class="nav-text" data-key="nav_peers">Пиры</span>
|
||||||
<span class="nav-text" data-key="nav_peers">Пиры</span>
|
</div>
|
||||||
</div>
|
<div class="nav-item" onclick="showSection('config')">
|
||||||
<div class="nav-item" onclick="showSection('config')">
|
<span class="nav-icon">⚙️</span>
|
||||||
<span class="nav-icon">⚙️</span>
|
<span class="nav-text" data-key="nav_config">Конфигурация</span>
|
||||||
<span class="nav-text" data-key="nav_config">Конфигурация</span>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
</nav>
|
</aside>
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<div id="status-section" class="content-section active">
|
<div id="status-section" class="content-section active">
|
||||||
<div class="status-card">
|
<div class="info-grid">
|
||||||
<h2 data-key="status_title">Состояние узла</h2>
|
<div class="info-card">
|
||||||
<p data-key="status_description">Информация о текущем состоянии вашего узла Yggdrasil</p>
|
<h3 data-key="node_info">Информация об узле</h3>
|
||||||
|
<p><span data-key="public_key">Публичный ключ</span>: <span id="node-key" class="copyable-field"
|
||||||
|
data-key="loading" onclick="copyNodeKey()"
|
||||||
|
data-key-title="copy_key_tooltip">Загрузка...</span></p>
|
||||||
|
<p><span data-key="version">Версия</span>: <span id="node-version"
|
||||||
|
data-key="loading">Загрузка...</span></p>
|
||||||
|
<p><span data-key="routing_entries">Записей маршрутизации</span>: <span id="routing-entries"
|
||||||
|
data-key="loading">Загрузка...</span></p>
|
||||||
|
<span id="node-key-full" data-value="" style="display: none;"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-grid">
|
<div class="info-card">
|
||||||
<div class="info-card">
|
<h3 data-key="network_info">Сетевая информация</h3>
|
||||||
<h3 data-key="node_info">Информация об узле</h3>
|
<p><span data-key="address">Адрес</span>: <span id="node-address" class="copyable-field"
|
||||||
<p><span data-key="public_key">Публичный ключ</span>: <span id="node-key"
|
data-key="loading" onclick="copyNodeAddress()"
|
||||||
data-key="loading">Загрузка...</span> <button onclick="copyNodeKey()"
|
data-key-title="copy_address_tooltip">Загрузка...</span>
|
||||||
style="margin-left: 8px; font-size: 12px;">📋</button></p>
|
</p>
|
||||||
<p><span data-key="version">Версия</span>: <span id="node-version"
|
<p><span data-key="subnet">Подсеть</span>: <span id="node-subnet" class="copyable-field"
|
||||||
data-key="loading">Загрузка...</span></p>
|
data-key="loading" onclick="copyNodeSubnet()"
|
||||||
<p><span data-key="routing_entries">Записей маршрутизации</span>: <span id="routing-entries"
|
data-key-title="copy_address_tooltip">Загрузка...</span>
|
||||||
data-key="loading">Загрузка...</span></p>
|
</p>
|
||||||
<span id="node-key-full" data-value="" style="display: none;"></span>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<h3 data-key="network_info">Сетевая информация</h3>
|
<h3 data-key="statistics">Статистика пиров</h3>
|
||||||
<p><span data-key="address">Адрес</span>: <span id="node-address"
|
<p><span data-key="total_peers">Всего пиров</span>: <span id="peers-count"
|
||||||
data-key="loading">Загрузка...</span></p>
|
data-key="loading">Загрузка...</span></p>
|
||||||
<p><span data-key="subnet">Подсеть</span>: <span id="node-subnet"
|
<p><span data-key="online_peers">Онлайн пиров</span>: <span id="peers-online"
|
||||||
data-key="loading">Загрузка...</span></p>
|
data-key="loading">Загрузка...</span></p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="info-card">
|
<div id="peers-section" class="content-section">
|
||||||
<h3 data-key="statistics">Статистика пиров</h3>
|
<div class="info-grid">
|
||||||
<p><span data-key="total_peers">Всего пиров</span>: <span id="peers-count"
|
<div class="info-card">
|
||||||
data-key="loading">Загрузка...</span></p>
|
<h3 data-key="add_peer">Добавить пир</h3>
|
||||||
<p><span data-key="online_peers">Онлайн пиров</span>: <span id="peers-online"
|
<p data-key="add_peer_description">Подключение к новому узлу</p>
|
||||||
data-key="loading">Загрузка...</span></p>
|
<button onclick="addPeer()" class="action-btn" data-key="add_peer_btn">Добавить пир</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="peers-section" class="content-section">
|
<div class="peers-container">
|
||||||
<div class="status-card">
|
<h3 data-key="connected_peers">Подключенные пиры</h3>
|
||||||
<h2 data-key="peers_title">Управление пирами</h2>
|
<div id="peers-list" class="peers-list">
|
||||||
<p data-key="peers_description">Просмотр и управление соединениями с пирами</p>
|
<div class="loading" data-key="loading">Загрузка...</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-grid">
|
|
||||||
<div class="info-card">
|
|
||||||
<h3 data-key="add_peer">Добавить пир</h3>
|
|
||||||
<p data-key="add_peer_description">Подключение к новому узлу</p>
|
|
||||||
<button onclick="addPeer()" class="action-btn" data-key="add_peer_btn">Добавить пир</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="peers-container">
|
|
||||||
<h3 data-key="connected_peers">Подключенные пиры</h3>
|
|
||||||
<div id="peers-list" class="peers-list">
|
|
||||||
<div class="loading" data-key="loading">Загрузка...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="config-section" class="content-section">
|
<div id="config-section" class="content-section">
|
||||||
<div class="status-card">
|
<div class="info-grid">
|
||||||
<h2 data-key="config_title">Конфигурация</h2>
|
<div class="info-card">
|
||||||
<p data-key="config_description">Настройки узла и параметры сети</p>
|
<h3 data-key="basic_settings">Основные настройки</h3>
|
||||||
|
<p data-key="basic_settings_description">Базовая конфигурация узла</p>
|
||||||
|
<small data-key="coming_soon">Функция в разработке...</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-grid">
|
<div class="info-card">
|
||||||
<div class="info-card">
|
<h3 data-key="network_settings">Сетевые настройки</h3>
|
||||||
<h3 data-key="basic_settings">Основные настройки</h3>
|
<p data-key="network_settings_description">Параметры сетевого взаимодействия</p>
|
||||||
<p data-key="basic_settings_description">Базовая конфигурация узла</p>
|
<small data-key="coming_soon">Функция в разработке...</small>
|
||||||
<small data-key="coming_soon">Функция в разработке...</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<h3 data-key="network_settings">Сетевые настройки</h3>
|
|
||||||
<p data-key="network_settings_description">Параметры сетевого взаимодействия</p>
|
|
||||||
<small data-key="coming_soon">Функция в разработке...</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
|
|
||||||
<div class="mobile-controls">
|
<div class="mobile-controls">
|
||||||
<div class="controls-group">
|
<div class="controls-group">
|
||||||
|
|
|
@ -51,5 +51,32 @@ window.translations.en = {
|
||||||
'notification_warning': 'Warning',
|
'notification_warning': 'Warning',
|
||||||
'notification_info': 'Information',
|
'notification_info': 'Information',
|
||||||
'dashboard_loaded': 'Dashboard loaded successfully',
|
'dashboard_loaded': 'Dashboard loaded successfully',
|
||||||
'welcome': 'Welcome'
|
'welcome': 'Welcome',
|
||||||
|
'copy_tooltip': 'Click to copy',
|
||||||
|
'copy_address_tooltip': 'Click to copy address',
|
||||||
|
'copy_key_tooltip': 'Click to copy key',
|
||||||
|
'copied_to_clipboard': 'Copied to clipboard',
|
||||||
|
'no_peers_connected': 'No peers connected',
|
||||||
|
'peer_connection': 'Connection',
|
||||||
|
'peer_performance': 'Performance',
|
||||||
|
'peer_traffic': 'Traffic',
|
||||||
|
'peer_uptime': 'Uptime',
|
||||||
|
'peer_port': 'Port',
|
||||||
|
'peer_priority': 'Priority',
|
||||||
|
'peer_latency': 'Latency',
|
||||||
|
'peer_cost': 'Cost',
|
||||||
|
'peer_quality': 'Quality',
|
||||||
|
'peer_received': '↓ Received',
|
||||||
|
'peer_sent': '↑ Sent',
|
||||||
|
'peer_total': 'Total',
|
||||||
|
'peer_remove': 'Remove',
|
||||||
|
'peer_status_online': 'Online',
|
||||||
|
'peer_status_offline': 'Offline',
|
||||||
|
'peer_direction_inbound': 'Inbound',
|
||||||
|
'peer_direction_outbound': 'Outbound',
|
||||||
|
'peer_quality_excellent': 'Excellent',
|
||||||
|
'peer_quality_good': 'Good',
|
||||||
|
'peer_quality_fair': 'Fair',
|
||||||
|
'peer_quality_poor': 'Poor',
|
||||||
|
'peer_quality_unknown': 'Unknown'
|
||||||
};
|
};
|
|
@ -37,7 +37,7 @@ window.translations.ru = {
|
||||||
'network_settings': 'Сетевые настройки',
|
'network_settings': 'Сетевые настройки',
|
||||||
'network_settings_description': 'Параметры сетевого взаимодействия',
|
'network_settings_description': 'Параметры сетевого взаимодействия',
|
||||||
'coming_soon': 'Функция в разработке...',
|
'coming_soon': 'Функция в разработке...',
|
||||||
'footer_text': '<strong>Yggdrasil Network</strong> • <span id="footer-version">loading...</span>',
|
'footer_text': '<strong>Yggdrasil Network</strong> • <span id="footer-version"></span>',
|
||||||
'logout_confirm': 'Вы уверены, что хотите выйти?',
|
'logout_confirm': 'Вы уверены, что хотите выйти?',
|
||||||
'theme_light': 'Светлая тема',
|
'theme_light': 'Светлая тема',
|
||||||
'theme_dark': 'Темная тема',
|
'theme_dark': 'Темная тема',
|
||||||
|
@ -51,5 +51,32 @@ window.translations.ru = {
|
||||||
'notification_warning': 'Предупреждение',
|
'notification_warning': 'Предупреждение',
|
||||||
'notification_info': 'Информация',
|
'notification_info': 'Информация',
|
||||||
'dashboard_loaded': 'Панель загружена успешно',
|
'dashboard_loaded': 'Панель загружена успешно',
|
||||||
'welcome': 'Добро пожаловать'
|
'welcome': 'Добро пожаловать',
|
||||||
|
'copy_tooltip': 'Нажмите для копирования',
|
||||||
|
'copy_address_tooltip': 'Нажмите для копирования адреса',
|
||||||
|
'copy_key_tooltip': 'Нажмите для копирования ключа',
|
||||||
|
'copied_to_clipboard': 'Скопировано в буфер обмена',
|
||||||
|
'no_peers_connected': 'Пиры не подключены',
|
||||||
|
'peer_connection': 'Соединение',
|
||||||
|
'peer_performance': 'Производительность',
|
||||||
|
'peer_traffic': 'Трафик',
|
||||||
|
'peer_uptime': 'Время работы',
|
||||||
|
'peer_port': 'Порт',
|
||||||
|
'peer_priority': 'Приоритет',
|
||||||
|
'peer_latency': 'Задержка',
|
||||||
|
'peer_cost': 'Стоимость',
|
||||||
|
'peer_quality': 'Качество',
|
||||||
|
'peer_received': '↓ Получено',
|
||||||
|
'peer_sent': '↑ Отправлено',
|
||||||
|
'peer_total': 'Всего',
|
||||||
|
'peer_remove': 'Удалить',
|
||||||
|
'peer_status_online': 'Онлайн',
|
||||||
|
'peer_status_offline': 'Офлайн',
|
||||||
|
'peer_direction_inbound': 'Входящее',
|
||||||
|
'peer_direction_outbound': 'Исходящее',
|
||||||
|
'peer_quality_excellent': 'Отличное',
|
||||||
|
'peer_quality_good': 'Хорошее',
|
||||||
|
'peer_quality_fair': 'Приемлемое',
|
||||||
|
'peer_quality_poor': 'Плохое',
|
||||||
|
'peer_quality_unknown': 'Неизвестно'
|
||||||
};
|
};
|
|
@ -5,12 +5,15 @@
|
||||||
|
|
||||||
// Global state variables
|
// Global state variables
|
||||||
let currentLanguage = localStorage.getItem('yggdrasil-language') || 'ru';
|
let currentLanguage = localStorage.getItem('yggdrasil-language') || 'ru';
|
||||||
|
|
||||||
|
// Export currentLanguage to window for access from other scripts
|
||||||
|
window.getCurrentLanguage = () => currentLanguage;
|
||||||
let currentTheme = localStorage.getItem('yggdrasil-theme') || 'light';
|
let currentTheme = localStorage.getItem('yggdrasil-theme') || 'light';
|
||||||
|
|
||||||
// Elements that should not be overwritten by translations when they contain data
|
// Elements that should not be overwritten by translations when they contain data
|
||||||
const dataElements = [
|
const dataElements = [
|
||||||
'node-key', 'node-version', 'routing-entries', 'node-address',
|
'node-key', 'node-version', 'routing-entries', 'node-address',
|
||||||
'node-subnet', 'peers-count', 'peers-online'
|
'node-subnet', 'peers-count', 'peers-online', 'footer-version'
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,7 +21,7 @@ const dataElements = [
|
||||||
*/
|
*/
|
||||||
function hasDataContent(element) {
|
function hasDataContent(element) {
|
||||||
const text = element.textContent.trim();
|
const text = element.textContent.trim();
|
||||||
const loadingTexts = ['Loading...', 'Загрузка...', 'N/A', ''];
|
const loadingTexts = ['Loading...', 'Загрузка...', 'N/A', '', 'unknown'];
|
||||||
return !loadingTexts.includes(text);
|
return !loadingTexts.includes(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,12 +42,34 @@ function updateTexts() {
|
||||||
if (window.translations && window.translations[currentLanguage] && window.translations[currentLanguage][key]) {
|
if (window.translations && window.translations[currentLanguage] && window.translations[currentLanguage][key]) {
|
||||||
// Special handling for footer_text which contains HTML
|
// Special handling for footer_text which contains HTML
|
||||||
if (key === 'footer_text') {
|
if (key === 'footer_text') {
|
||||||
|
// Save current version value if it exists
|
||||||
|
const versionElement = document.getElementById('footer-version');
|
||||||
|
const currentVersion = versionElement ? versionElement.textContent : '';
|
||||||
|
|
||||||
|
// Update footer text
|
||||||
element.innerHTML = window.translations[currentLanguage][key];
|
element.innerHTML = window.translations[currentLanguage][key];
|
||||||
|
|
||||||
|
// Restore version value if it was there
|
||||||
|
if (currentVersion && currentVersion !== '' && currentVersion !== 'unknown') {
|
||||||
|
const newVersionElement = document.getElementById('footer-version');
|
||||||
|
if (newVersionElement) {
|
||||||
|
newVersionElement.textContent = currentVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
element.textContent = window.translations[currentLanguage][key];
|
element.textContent = window.translations[currentLanguage][key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle title translations
|
||||||
|
const titleElements = document.querySelectorAll('[data-key-title]');
|
||||||
|
titleElements.forEach(element => {
|
||||||
|
const titleKey = element.getAttribute('data-key-title');
|
||||||
|
if (window.translations && window.translations[currentLanguage] && window.translations[currentLanguage][titleKey]) {
|
||||||
|
element.title = window.translations[currentLanguage][titleKey];
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -123,19 +123,30 @@
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
overflow: hidden; /* Prevent body scroll */
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
display: grid;
|
||||||
margin: 0 auto;
|
grid-template-columns: 250px 1fr;
|
||||||
padding: 20px;
|
grid-template-rows: auto 1fr auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"header header"
|
||||||
|
"sidebar main"
|
||||||
|
"footer footer";
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
margin-bottom: 40px;
|
grid-area: header;
|
||||||
color: var(--text-white);
|
color: var(--text-white);
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
||||||
|
box-shadow: 0 2px 4px var(--shadow-medium);
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-content {
|
.header-content {
|
||||||
|
@ -244,8 +255,6 @@ header p {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
background: var(--bg-nav-active);
|
background: var(--bg-nav-active);
|
||||||
color: var(--text-white);
|
color: var(--text-white);
|
||||||
|
@ -419,19 +428,13 @@ header p {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
min-width: 250px;
|
grid-area: sidebar;
|
||||||
background: var(--bg-sidebar);
|
background: var(--bg-sidebar);
|
||||||
border-radius: 4px;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 2px 8px var(--shadow-heavy);
|
box-shadow: 2px 0 4px var(--shadow-light);
|
||||||
border: 1px solid var(--border-sidebar);
|
border-right: 1px solid var(--border-sidebar);
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-menu {
|
.nav-menu {
|
||||||
|
@ -474,12 +477,10 @@ header p {
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
grid-area: main;
|
||||||
background: var(--bg-main-content);
|
background: var(--bg-main-content);
|
||||||
border-radius: 4px;
|
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
box-shadow: 0 2px 8px var(--shadow-dark);
|
overflow-y: auto;
|
||||||
border: 1px solid var(--border-main);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-section {
|
.content-section {
|
||||||
|
@ -588,14 +589,26 @@ header p {
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
|
grid-area: footer;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--text-white);
|
color: var(--text-white);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
padding: 15px;
|
||||||
|
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
||||||
|
border-top: 1px solid var(--border-footer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
overflow: auto; /* Allow body scroll on mobile */
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
display: block; /* Reset grid for mobile */
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-content {
|
.header-content {
|
||||||
|
@ -616,6 +629,11 @@ footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 20px 10px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
header h1 {
|
header h1 {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
}
|
}
|
||||||
|
@ -624,20 +642,13 @@ footer {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
min-width: auto;
|
min-width: auto;
|
||||||
order: 1;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--border-sidebar);
|
||||||
|
box-shadow: 0 2px 4px var(--shadow-light);
|
||||||
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-menu {
|
.nav-menu {
|
||||||
|
@ -667,7 +678,9 @@ footer {
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
padding: 20px 15px;
|
padding: 20px 15px;
|
||||||
order: 2;
|
overflow-y: visible;
|
||||||
|
height: auto;
|
||||||
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-card {
|
.status-card {
|
||||||
|
@ -721,31 +734,73 @@ footer {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-address-section {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.peer-address {
|
.peer-address {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
margin-bottom: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-key {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-status-section {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.peer-status {
|
.peer-status {
|
||||||
align-self: flex-start;
|
font-size: 0.7rem;
|
||||||
font-size: 0.75rem;
|
padding: 0.2rem 0.5rem;
|
||||||
padding: 0.2rem 0.6rem;
|
}
|
||||||
|
|
||||||
|
.peer-direction {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.peer-uri {
|
.peer-uri {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
padding: 0.375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.peer-stats {
|
.peer-info-grid {
|
||||||
flex-direction: column;
|
grid-template-columns: 1fr;
|
||||||
gap: 0.5rem;
|
gap: 0.75rem;
|
||||||
|
margin-top: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-info-section {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-info-title {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
margin-bottom: 0.375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.peer-stats span {
|
.peer-info-stats {
|
||||||
padding: 0.2rem 0.4rem;
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -758,11 +813,10 @@ footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 0px 0px 20px 0px;
|
margin: 0px 0px 20px 0px;
|
||||||
order: 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
order: 4;
|
padding: 15px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-controls .controls-group {
|
.mobile-controls .controls-group {
|
||||||
|
@ -882,6 +936,30 @@ footer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Alternative background solution for mobile devices */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
position: relative;
|
||||||
|
background: none;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Landscape orientation optimizations for mobile */
|
/* Landscape orientation optimizations for mobile */
|
||||||
@media (max-width: 768px) and (orientation: landscape) {
|
@media (max-width: 768px) and (orientation: landscape) {
|
||||||
.header-actions {
|
.header-actions {
|
||||||
|
@ -893,14 +971,18 @@ footer {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout {
|
.container {
|
||||||
flex-direction: row;
|
display: grid;
|
||||||
|
grid-template-columns: 200px 1fr;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
order: 1;
|
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
|
border-right: 1px solid var(--border-sidebar);
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-menu {
|
.nav-menu {
|
||||||
|
@ -918,8 +1000,7 @@ footer {
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
order: 2;
|
overflow-y: auto;
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -957,8 +1038,14 @@ footer {
|
||||||
.peer-header {
|
.peer-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.75rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-address-section {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.peer-address {
|
.peer-address {
|
||||||
|
@ -966,19 +1053,32 @@ footer {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--text-heading);
|
color: var(--text-heading);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-key {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-status-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.peer-status {
|
.peer-status {
|
||||||
padding: 0.25rem 0.75rem;
|
padding: 0.25rem 0.75rem;
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.peer-status.status-online {
|
.peer-status.status-online {
|
||||||
background: var(--bg-success);
|
|
||||||
color: var(--text-success);
|
color: var(--text-success);
|
||||||
border: 1px solid var(--border-success);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.peer-status.status-offline {
|
.peer-status.status-offline {
|
||||||
|
@ -987,8 +1087,24 @@ footer {
|
||||||
border: 1px solid var(--border-error);
|
border: 1px solid var(--border-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.peer-direction {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-direction.inbound {
|
||||||
|
background: var(--bg-nav-item);
|
||||||
|
color: var(--text-nav);
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-direction.outbound {
|
||||||
|
background: var(--bg-nav-item);
|
||||||
|
color: var(--text-nav);
|
||||||
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .peer-status.status-online {
|
[data-theme="dark"] .peer-status.status-online {
|
||||||
background: var(--bg-success);
|
|
||||||
color: var(--text-success);
|
color: var(--text-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1000,7 +1116,7 @@ footer {
|
||||||
.peer-details {
|
.peer-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.peer-uri {
|
.peer-uri {
|
||||||
|
@ -1008,20 +1124,78 @@ footer {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
padding: 0.5rem;
|
||||||
|
|
||||||
.peer-stats {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.peer-stats span {
|
|
||||||
background: var(--bg-nav-item);
|
background: var(--bg-nav-item);
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid var(--border-card);
|
border: 1px solid var(--border-nav-item);
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-info-section {
|
||||||
|
background: var(--bg-nav-item);
|
||||||
|
border: 1px solid var(--border-nav-item);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-info-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-heading);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-info-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
color: var(--text-body);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--text-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quality indicators */
|
||||||
|
.quality-excellent .info-value {
|
||||||
|
color: var(--text-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-good .info-value {
|
||||||
|
color: var(--bg-nav-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-fair .info-value {
|
||||||
|
color: var(--text-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-poor .info-value {
|
||||||
|
color: var(--text-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-unknown .info-value {
|
||||||
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.peer-remove-btn {
|
.peer-remove-btn {
|
||||||
|
@ -1062,6 +1236,41 @@ footer {
|
||||||
color: var(--text-heading);
|
color: var(--text-heading);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Copyable fields styling */
|
||||||
|
.copyable-field {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyable-field:hover {
|
||||||
|
background: var(--bg-nav-hover);
|
||||||
|
color: var(--border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyable-field:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Peer copyable fields */
|
||||||
|
.peer-address.copyable, .peer-key.copyable {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-address.copyable:hover, .peer-key.copyable:hover {
|
||||||
|
background: var(--bg-nav-hover);
|
||||||
|
color: var(--border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.peer-address.copyable:active, .peer-key.copyable:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
/* Copy button styling */
|
/* Copy button styling */
|
||||||
button[onclick="copyNodeKey()"] {
|
button[onclick="copyNodeKey()"] {
|
||||||
background: var(--bg-nav-item);
|
background: var(--bg-nav-item);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue