diff --git a/src/webui/static/api.js b/src/webui/static/api.js index 9ba45008..2c0e10e2 100644 --- a/src/webui/static/api.js +++ b/src/webui/static/api.js @@ -172,8 +172,94 @@ class YggdrasilUtils { * @returns {string} - Status text */ 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'; } + + /** + * 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 diff --git a/src/webui/static/app.js b/src/webui/static/app.js index 38f9eb82..7e44f93a 100644 --- a/src/webui/static/app.js +++ b/src/webui/static/app.js @@ -54,8 +54,10 @@ function updateNodeInfoDisplay(info) { // Update footer version 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-address', info.address || ''); + updateElementData('node-subnet', info.subnet || ''); } /** @@ -75,7 +77,11 @@ function updatePeersDisplay(data) { updateElementText('peers-online', onlineCount.toString()); if (!data.peers || data.peers.length === 0) { - peersContainer.innerHTML = '
No peers connected
'; + 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 = `
${message}
`; return; } @@ -83,12 +89,24 @@ function updatePeersDisplay(data) { const peerElement = createPeerElement(peer); 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 window.updateNodeInfoDisplay = updateNodeInfoDisplay; 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 */ @@ -98,21 +116,101 @@ function createPeerElement(peer) { const statusClass = yggUtils.getPeerStatusClass(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 = `
-
${peer.address || 'N/A'}
-
${statusText}
+
+
${peer.address || 'N/A'}
+
${yggUtils.formatPublicKey(peer.key) || 'N/A'}
+
+
+
${statusText}
+
+ ${direction.icon} ${direction.text} +
+
${peer.remote || 'N/A'}
-
- ↓ ${yggUtils.formatBytes(peer.bytes_recvd || 0)} - ↑ ${yggUtils.formatBytes(peer.bytes_sent || 0)} - ${peer.up && peer.latency ? `RTT: ${(peer.latency / 1000000).toFixed(1)}ms` : ''} +
+
+
${labels.connection}
+
+ + ${labels.uptime}: + ${uptimeText} + + + ${labels.port}: + ${peer.port || 'N/A'} + + + ${labels.priority}: + ${peer.priority !== undefined ? peer.priority : 'N/A'} + +
+
+
+
${labels.performance}
+
+ + ${labels.latency}: + ${latencyText} + + + ${labels.cost}: + ${peer.cost !== undefined ? peer.cost : 'N/A'} + + + ${labels.quality}: + ${quality.text} + +
+
+
+
${labels.traffic}
+
+ + ${labels.received}: + ${yggUtils.formatBytes(peer.bytes_recvd || 0)} + + + ${labels.sent}: + ${yggUtils.formatBytes(peer.bytes_sent || 0)} + + + ${labels.total}: + ${yggUtils.formatBytes((peer.bytes_recvd || 0) + (peer.bytes_sent || 0))} + +
+
- ${peer.remote ? `` : ''} + ${peer.remote ? `` : ''} `; return div; @@ -193,7 +291,11 @@ function updateElementData(id, data) { async function copyToClipboard(text) { try { 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) { console.error('Failed to copy:', error); 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 */ @@ -250,7 +396,11 @@ async function initializeApp() { // Load initial data 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 startAutoRefresh(); diff --git a/src/webui/static/index.html b/src/webui/static/index.html index 00ea40ee..f25cae1a 100644 --- a/src/webui/static/index.html +++ b/src/webui/static/index.html @@ -34,106 +34,93 @@
-
- + -
-
-
-

Состояние узла

-

Информация о текущем состоянии вашего узла Yggdrasil

+
+
+
+
+

Информация об узле

+

Публичный ключ: Загрузка...

+

Версия: Загрузка...

+

Записей маршрутизации: Загрузка...

+
-
-
-

Информация об узле

-

Публичный ключ: Загрузка...

-

Версия: Загрузка...

-

Записей маршрутизации: Загрузка...

- -
+
+

Сетевая информация

+

Адрес: Загрузка... +

+

Подсеть: Загрузка... +

+
-
-

Сетевая информация

-

Адрес: Загрузка...

-

Подсеть: Загрузка...

-
+
+

Статистика пиров

+

Всего пиров: Загрузка...

+

Онлайн пиров: Загрузка...

+
+
+
-
-

Статистика пиров

-

Всего пиров: Загрузка...

-

Онлайн пиров: Загрузка...

-
+
+
+
+

Добавить пир

+

Подключение к новому узлу

+
-
-
-

Управление пирами

-

Просмотр и управление соединениями с пирами

-
- -
-
-

Добавить пир

-

Подключение к новому узлу

- -
-
- -
-

Подключенные пиры

-
-
Загрузка...
-
+
+

Подключенные пиры

+
+
Загрузка...
+
-
-
-

Конфигурация

-

Настройки узла и параметры сети

+
+
+
+

Основные настройки

+

Базовая конфигурация узла

+ Функция в разработке...
-
-
-

Основные настройки

-

Базовая конфигурация узла

- Функция в разработке... -
- -
-

Сетевые настройки

-

Параметры сетевого взаимодействия

- Функция в разработке... -
+
+

Сетевые настройки

+

Параметры сетевого взаимодействия

+ Функция в разработке...
-
-
+
+
diff --git a/src/webui/static/lang/en.js b/src/webui/static/lang/en.js index d3826993..c4bc3c39 100644 --- a/src/webui/static/lang/en.js +++ b/src/webui/static/lang/en.js @@ -51,5 +51,32 @@ window.translations.en = { 'notification_warning': 'Warning', 'notification_info': 'Information', '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' }; \ No newline at end of file diff --git a/src/webui/static/lang/ru.js b/src/webui/static/lang/ru.js index bc52fd99..10fb59c0 100644 --- a/src/webui/static/lang/ru.js +++ b/src/webui/static/lang/ru.js @@ -37,7 +37,7 @@ window.translations.ru = { 'network_settings': 'Сетевые настройки', 'network_settings_description': 'Параметры сетевого взаимодействия', 'coming_soon': 'Функция в разработке...', - 'footer_text': 'Yggdrasil Networkloading...', + 'footer_text': 'Yggdrasil Network', 'logout_confirm': 'Вы уверены, что хотите выйти?', 'theme_light': 'Светлая тема', 'theme_dark': 'Темная тема', @@ -51,5 +51,32 @@ window.translations.ru = { 'notification_warning': 'Предупреждение', 'notification_info': 'Информация', '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': 'Неизвестно' }; \ No newline at end of file diff --git a/src/webui/static/main.js b/src/webui/static/main.js index 6b47103a..c029d1e6 100644 --- a/src/webui/static/main.js +++ b/src/webui/static/main.js @@ -5,12 +5,15 @@ // Global state variables 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'; // Elements that should not be overwritten by translations when they contain data const dataElements = [ '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) { const text = element.textContent.trim(); - const loadingTexts = ['Loading...', 'Загрузка...', 'N/A', '']; + const loadingTexts = ['Loading...', 'Загрузка...', 'N/A', '', 'unknown']; return !loadingTexts.includes(text); } @@ -39,12 +42,34 @@ function updateTexts() { if (window.translations && window.translations[currentLanguage] && window.translations[currentLanguage][key]) { // Special handling for footer_text which contains HTML 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]; + + // Restore version value if it was there + if (currentVersion && currentVersion !== '' && currentVersion !== 'unknown') { + const newVersionElement = document.getElementById('footer-version'); + if (newVersionElement) { + newVersionElement.textContent = currentVersion; + } + } } else { 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]; + } + }); } /** diff --git a/src/webui/static/style.css b/src/webui/static/style.css index d2379896..11597f87 100644 --- a/src/webui/static/style.css +++ b/src/webui/static/style.css @@ -123,19 +123,30 @@ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%); - min-height: 100vh; + height: 100vh; color: var(--text-primary); + overflow: hidden; /* Prevent body scroll */ } .container { - max-width: 1200px; - margin: 0 auto; - padding: 20px; + display: grid; + grid-template-columns: 250px 1fr; + grid-template-rows: auto 1fr auto; + grid-template-areas: + "header header" + "sidebar main" + "footer footer"; + height: 100vh; + width: 100vw; } header { - margin-bottom: 40px; + grid-area: header; 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 { @@ -244,8 +255,6 @@ header p { opacity: 0.9; } - - .action-btn { background: var(--bg-nav-active); color: var(--text-white); @@ -419,19 +428,13 @@ header p { } } -.layout { - display: flex; - gap: 20px; - margin-bottom: 20px; -} - .sidebar { - min-width: 250px; + grid-area: sidebar; background: var(--bg-sidebar); - border-radius: 4px; padding: 20px; - box-shadow: 0 2px 8px var(--shadow-heavy); - border: 1px solid var(--border-sidebar); + box-shadow: 2px 0 4px var(--shadow-light); + border-right: 1px solid var(--border-sidebar); + overflow-y: auto; } .nav-menu { @@ -474,12 +477,10 @@ header p { } .main-content { - flex: 1; + grid-area: main; background: var(--bg-main-content); - border-radius: 4px; padding: 30px; - box-shadow: 0 2px 8px var(--shadow-dark); - border: 1px solid var(--border-main); + overflow-y: auto; } .content-section { @@ -588,14 +589,26 @@ header p { } footer { + grid-area: footer; text-align: center; color: var(--text-white); 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) { + body { + overflow: auto; /* Allow body scroll on mobile */ + } + .container { + display: block; /* Reset grid for mobile */ padding: 10px; + height: auto; + width: auto; + min-height: 100vh; } .header-content { @@ -616,6 +629,11 @@ footer { text-align: center; } + header { + padding: 20px 10px; + margin-bottom: 40px; + } + header h1 { font-size: 1.8rem; } @@ -624,20 +642,13 @@ footer { font-size: 1rem; } - .layout { - flex-direction: column; - gap: 15px; - } - - .container { - display: flex; - flex-direction: column; - } - .sidebar { min-width: auto; - order: 1; 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 { @@ -667,7 +678,9 @@ footer { .main-content { padding: 20px 15px; - order: 2; + overflow-y: visible; + height: auto; + margin-bottom: 15px; } .status-card { @@ -721,31 +734,73 @@ footer { flex-direction: column; align-items: flex-start; gap: 8px; + margin-bottom: 0.5rem; + } + + .peer-address-section { + width: 100%; } .peer-address { font-size: 0.8rem; 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 { - align-self: flex-start; - font-size: 0.75rem; - padding: 0.2rem 0.6rem; + font-size: 0.7rem; + padding: 0.2rem 0.5rem; + } + + .peer-direction { + font-size: 0.7rem; + padding: 0.15rem 0.4rem; } .peer-uri { font-size: 0.75rem; + padding: 0.375rem; } - .peer-stats { - flex-direction: column; - gap: 0.5rem; + .peer-info-grid { + grid-template-columns: 1fr; + gap: 0.75rem; + margin-top: 0.375rem; + } + + .peer-info-section { + padding: 0.5rem; + } + + .peer-info-title { font-size: 0.75rem; + margin-bottom: 0.375rem; } - .peer-stats span { - padding: 0.2rem 0.4rem; + .peer-info-stats { + gap: 0.25rem; + } + + .info-item { + font-size: 0.7rem; + } + + .info-label { + flex: 1; + } + + .info-value { font-size: 0.7rem; } @@ -758,11 +813,10 @@ footer { display: flex; justify-content: center; margin: 0px 0px 20px 0px; - order: 3; } footer { - order: 4; + padding: 15px 10px; } .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 */ @media (max-width: 768px) and (orientation: landscape) { .header-actions { @@ -893,14 +971,18 @@ footer { display: none; } - .layout { - flex-direction: row; + .container { + display: grid; + grid-template-columns: 200px 1fr; + grid-template-rows: auto 1fr auto; } .sidebar { - order: 1; min-width: 200px; max-width: 250px; + border-right: 1px solid var(--border-sidebar); + border-bottom: none; + margin-bottom: 0; } .nav-menu { @@ -918,8 +1000,7 @@ footer { } .main-content { - order: 2; - flex: 1; + overflow-y: auto; } } @@ -957,8 +1038,14 @@ footer { .peer-header { display: flex; justify-content: space-between; - align-items: center; - margin-bottom: 0.5rem; + align-items: flex-start; + margin-bottom: 0.75rem; + gap: 1rem; +} + +.peer-address-section { + flex: 1; + min-width: 0; } .peer-address { @@ -966,19 +1053,32 @@ footer { font-weight: bold; color: var(--text-heading); 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 { padding: 0.25rem 0.75rem; - border-radius: 12px; font-size: 0.8rem; font-weight: bold; + white-space: nowrap; } .peer-status.status-online { - background: var(--bg-success); color: var(--text-success); - border: 1px solid var(--border-success); } .peer-status.status-offline { @@ -987,8 +1087,24 @@ footer { 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 { - background: var(--bg-success); color: var(--text-success); } @@ -1000,7 +1116,7 @@ footer { .peer-details { display: flex; flex-direction: column; - gap: 0.5rem; + gap: 0.75rem; } .peer-uri { @@ -1008,20 +1124,78 @@ footer { font-size: 0.85rem; color: var(--text-muted); word-break: break-all; -} - -.peer-stats { - display: flex; - gap: 1rem; - font-size: 0.8rem; - color: var(--text-muted); -} - -.peer-stats span { + padding: 0.5rem; background: var(--bg-nav-item); - padding: 0.25rem 0.5rem; 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 { @@ -1062,6 +1236,41 @@ footer { 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 */ button[onclick="copyNodeKey()"] { background: var(--bg-nav-item);