From 2b3b4c39d251203c6c0b3787a557bd579358d4f6 Mon Sep 17 00:00:00 2001 From: Andy Oknen Date: Wed, 30 Jul 2025 16:09:40 +0000 Subject: [PATCH] Expose global state and update peer display logic in WebUI --- src/webui/static/app.js | 30 ++-- src/webui/static/index.html | 148 +------------------- src/webui/static/lang/en.js | 14 +- src/webui/static/lang/ru.js | 14 +- src/webui/static/main.js | 266 ++++++++++++++++++++++++++++++++++++ src/webui/static/style.css | 2 +- 6 files changed, 312 insertions(+), 162 deletions(-) create mode 100644 src/webui/static/main.js diff --git a/src/webui/static/app.js b/src/webui/static/app.js index 0444adcf..4a370874 100644 --- a/src/webui/static/app.js +++ b/src/webui/static/app.js @@ -3,9 +3,9 @@ * Integrates admin API with the user interface */ -// Global state -let nodeInfo = null; -let peersData = null; +// Global state - expose to window for access from other scripts +window.nodeInfo = null; +window.peersData = null; let isLoading = false; /** @@ -14,7 +14,7 @@ let isLoading = false; async function loadNodeInfo() { try { const info = await window.yggAPI.getSelf(); - nodeInfo = info; + window.nodeInfo = info; updateNodeInfoDisplay(info); return info; } catch (error) { @@ -30,7 +30,7 @@ async function loadNodeInfo() { async function loadPeers() { try { const data = await window.yggAPI.getPeers(); - peersData = data; + window.peersData = data; updatePeersDisplay(data); return data; } catch (error) { @@ -64,6 +64,13 @@ function updatePeersDisplay(data) { peersContainer.innerHTML = ''; + // Always update peer counts, even if no peers + const peersCount = data.peers ? data.peers.length : 0; + const onlineCount = data.peers ? data.peers.filter(p => p.up).length : 0; + + updateElementText('peers-count', peersCount.toString()); + updateElementText('peers-online', onlineCount.toString()); + if (!data.peers || data.peers.length === 0) { peersContainer.innerHTML = '
No peers connected
'; return; @@ -73,12 +80,12 @@ function updatePeersDisplay(data) { const peerElement = createPeerElement(peer); peersContainer.appendChild(peerElement); }); - - // Update peer count - updateElementText('peers-count', data.peers.length.toString()); - updateElementText('peers-online', data.peers.filter(p => p.up).length.toString()); } +// Expose update functions to window for access from other scripts +window.updateNodeInfoDisplay = updateNodeInfoDisplay; +window.updatePeersDisplay = updatePeersDisplay; + /** * Create HTML element for a single peer */ @@ -232,6 +239,11 @@ async function initializeApp() { } isLoading = true; + + // Initialize peer counts to 0 immediately to replace "Loading..." text + updateElementText('peers-count', '0'); + updateElementText('peers-online', '0'); + showInfo('Loading dashboard...'); // Load initial data diff --git a/src/webui/static/index.html b/src/webui/static/index.html index 9e41333c..b4e6bc3c 100644 --- a/src/webui/static/index.html +++ b/src/webui/static/index.html @@ -7,9 +7,10 @@ Yggdrasil Web Interface + - + @@ -145,152 +146,7 @@
- \ No newline at end of file diff --git a/src/webui/static/lang/en.js b/src/webui/static/lang/en.js index 3266c606..cb30a1e0 100644 --- a/src/webui/static/lang/en.js +++ b/src/webui/static/lang/en.js @@ -8,19 +8,28 @@ window.translations.en = { 'nav_config': 'Configuration', 'status_title': 'Node Status', 'status_active': 'Active', - 'status_description': 'WebUI is running and accessible', + 'status_description': 'Information about your Yggdrasil node current status', + 'node_info': 'Node Information', + 'public_key': 'Public Key', + 'version': 'Version', + 'routing_entries': 'Routing Entries', + 'loading': 'Loading...', 'network_info': 'Network Information', 'address': 'Address', 'subnet': 'Subnet', - 'statistics': 'Statistics', + 'statistics': 'Peer Statistics', + 'total_peers': 'Total Peers', + 'online_peers': 'Online Peers', 'uptime': 'Uptime', 'connections': 'Active connections', 'peers_title': 'Peer Management', 'peers_description': 'View and manage peer connections', + 'connected_peers': 'Connected Peers', 'active_peers': 'Active Peers', 'active_connections': 'Number of active connections', 'add_peer': 'Add Peer', 'add_peer_description': 'Connect to a new node', + 'add_peer_btn': 'Add Peer', 'config_title': 'Configuration', 'config_description': 'Node settings and network parameters', 'basic_settings': 'Basic Settings', @@ -37,7 +46,6 @@ window.translations.en = { 'access_dashboard': 'Access Dashboard', 'error_invalid_password': 'Invalid password. Please try again.', 'error_too_many_attempts': 'Too many failed attempts. Please wait 1 minute before trying again.', - 'add_peer_btn': 'Add Peer', 'notification_success': 'Success', 'notification_error': 'Error', 'notification_warning': 'Warning', diff --git a/src/webui/static/lang/ru.js b/src/webui/static/lang/ru.js index 40b2eee3..5cd95181 100644 --- a/src/webui/static/lang/ru.js +++ b/src/webui/static/lang/ru.js @@ -8,19 +8,28 @@ window.translations.ru = { 'nav_config': 'Конфигурация', 'status_title': 'Состояние узла', 'status_active': 'Активен', - 'status_description': 'WebUI запущен и доступен', + 'status_description': 'Информация о текущем состоянии вашего узла Yggdrasil', + 'node_info': 'Информация об узле', + 'public_key': 'Публичный ключ', + 'version': 'Версия', + 'routing_entries': 'Записей маршрутизации', + 'loading': 'Загрузка...', 'network_info': 'Сетевая информация', 'address': 'Адрес', 'subnet': 'Подсеть', - 'statistics': 'Статистика', + 'statistics': 'Статистика пиров', + 'total_peers': 'Всего пиров', + 'online_peers': 'Онлайн пиров', 'uptime': 'Время работы', 'connections': 'Активных соединений', 'peers_title': 'Управление пирами', 'peers_description': 'Просмотр и управление соединениями с пирами', + 'connected_peers': 'Подключенные пиры', 'active_peers': 'Активные пиры', 'active_connections': 'Количество активных соединений', 'add_peer': 'Добавить пир', 'add_peer_description': 'Подключение к новому узлу', + 'add_peer_btn': 'Добавить пир', 'config_title': 'Конфигурация', 'config_description': 'Настройки узла и параметры сети', 'basic_settings': 'Основные настройки', @@ -37,7 +46,6 @@ window.translations.ru = { 'access_dashboard': 'Войти в панель', 'error_invalid_password': 'Неверный пароль. Попробуйте снова.', 'error_too_many_attempts': 'Слишком много неудачных попыток. Подождите 1 минуту перед повторной попыткой.', - 'add_peer_btn': 'Добавить пир', 'notification_success': 'Успешно', 'notification_error': 'Ошибка', 'notification_warning': 'Предупреждение', diff --git a/src/webui/static/main.js b/src/webui/static/main.js new file mode 100644 index 00000000..6f257d92 --- /dev/null +++ b/src/webui/static/main.js @@ -0,0 +1,266 @@ +/** + * Main JavaScript logic for Yggdrasil Web Interface + * Handles language switching, theme management, notifications, and UI interactions + */ + +// Global state variables +let currentLanguage = localStorage.getItem('yggdrasil-language') || 'ru'; +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' +]; + +/** + * Check if an element contains actual data (not just loading text or empty) + */ +function hasDataContent(element) { + const text = element.textContent.trim(); + const loadingTexts = ['Loading...', 'Загрузка...', 'N/A', '']; + return !loadingTexts.includes(text); +} + +/** + * Update all text elements based on current language + */ +function updateTexts() { + const elements = document.querySelectorAll('[data-key]'); + elements.forEach(element => { + const key = element.getAttribute('data-key'); + const elementId = element.id; + + // Skip data elements that already have content loaded + if (elementId && dataElements.includes(elementId) && hasDataContent(element)) { + return; + } + + if (window.translations && window.translations[currentLanguage] && window.translations[currentLanguage][key]) { + element.textContent = window.translations[currentLanguage][key]; + } + }); +} + +/** + * Refresh displayed data after language change + */ +function refreshDataDisplay() { + // If we have node info, refresh its display + if (window.nodeInfo) { + window.updateNodeInfoDisplay(window.nodeInfo); + } + + // If we have peers data, refresh its display + if (window.peersData) { + window.updatePeersDisplay(window.peersData); + } +} + +/** + * Toggle between light and dark theme + */ +function toggleTheme() { + currentTheme = currentTheme === 'light' ? 'dark' : 'light'; + applyTheme(); + localStorage.setItem('yggdrasil-theme', currentTheme); +} + +/** + * Apply the current theme to the document + */ +function applyTheme() { + document.documentElement.setAttribute('data-theme', currentTheme); + const themeBtn = document.getElementById('theme-btn'); + if (themeBtn) { + const icon = themeBtn.querySelector('.theme-icon'); + if (icon) { + icon.textContent = currentTheme === 'light' ? '🌙' : '☀️'; + } + } +} + +/** + * Switch application language + * @param {string} lang - Language code (ru, en) + */ +function switchLanguage(lang) { + currentLanguage = lang; + localStorage.setItem('yggdrasil-language', lang); + + // Update button states + document.querySelectorAll('.lang-btn').forEach(btn => btn.classList.remove('active')); + document.getElementById('lang-' + lang).classList.add('active'); + + // Update all texts + updateTexts(); + + // Refresh data display to preserve loaded data + refreshDataDisplay(); +} + +/** + * Show a specific content section and hide others + * @param {string} sectionName - Name of the section to show + */ +function showSection(sectionName) { + // Hide all sections + const sections = document.querySelectorAll('.content-section'); + sections.forEach(section => section.classList.remove('active')); + + // Remove active class from all nav items + const navItems = document.querySelectorAll('.nav-item'); + navItems.forEach(item => item.classList.remove('active')); + + // Show selected section + const targetSection = document.getElementById(sectionName + '-section'); + if (targetSection) { + targetSection.classList.add('active'); + } + + // Add active class to clicked nav item + if (event && event.target) { + event.target.closest('.nav-item').classList.add('active'); + } +} + +/** + * Logout function (placeholder) + */ +function logout() { + if (confirm('Are you sure you want to logout?')) { + // Clear stored preferences + localStorage.removeItem('yggdrasil-language'); + localStorage.removeItem('yggdrasil-theme'); + + // Redirect or refresh + window.location.reload(); + } +} + +// Notification system +let notificationId = 0; + +/** + * Show a notification to the user + * @param {string} message - Notification message + * @param {string} type - Notification type (info, success, error, warning) + * @param {string} title - Optional custom title + * @param {number} duration - Auto-hide duration in milliseconds (0 = no auto-hide) + * @returns {number} Notification ID + */ +function showNotification(message, type = 'info', title = null, duration = 5000) { + const container = document.getElementById('notifications-container'); + const id = ++notificationId; + + const icons = { + success: '✅', + error: '❌', + warning: '⚠️', + info: 'ℹ️' + }; + + const titles = { + success: window.translations[currentLanguage]['notification_success'] || 'Success', + error: window.translations[currentLanguage]['notification_error'] || 'Error', + warning: window.translations[currentLanguage]['notification_warning'] || 'Warning', + info: window.translations[currentLanguage]['notification_info'] || 'Information' + }; + + const notification = document.createElement('div'); + notification.className = `notification ${type}`; + notification.id = `notification-${id}`; + + notification.innerHTML = ` +
${icons[type] || icons.info}
+
+
${title || titles[type]}
+
${message}
+
+ + `; + + container.appendChild(notification); + + // Auto remove after duration + if (duration > 0) { + setTimeout(() => { + removeNotification(id); + }, duration); + } + + return id; +} + +/** + * Remove a notification by ID + * @param {number} id - Notification ID to remove + */ +function removeNotification(id) { + const notification = document.getElementById(`notification-${id}`); + if (notification) { + notification.classList.add('removing'); + setTimeout(() => { + if (notification.parentNode) { + notification.parentNode.removeChild(notification); + } + }, 300); + } +} + +/** + * Show success notification + * @param {string} message - Success message + * @param {string} title - Optional custom title + * @returns {number} Notification ID + */ +function showSuccess(message, title = null) { + return showNotification(message, 'success', title); +} + +/** + * Show error notification + * @param {string} message - Error message + * @param {string} title - Optional custom title + * @returns {number} Notification ID + */ +function showError(message, title = null) { + return showNotification(message, 'error', title); +} + +/** + * Show warning notification + * @param {string} message - Warning message + * @param {string} title - Optional custom title + * @returns {number} Notification ID + */ +function showWarning(message, title = null) { + return showNotification(message, 'warning', title); +} + +/** + * Show info notification + * @param {string} message - Info message + * @param {string} title - Optional custom title + * @returns {number} Notification ID + */ +function showInfo(message, title = null) { + return showNotification(message, 'info', title); +} + +/** + * Initialize the application when DOM is loaded + */ +function initializeMain() { + // Set active language button + document.getElementById('lang-' + currentLanguage).classList.add('active'); + + // Update all texts + updateTexts(); + + // Apply saved theme + applyTheme(); +} + +// Initialize when DOM is ready +document.addEventListener('DOMContentLoaded', initializeMain); \ No newline at end of file diff --git a/src/webui/static/style.css b/src/webui/static/style.css index 4b350f3a..acdc13f6 100644 --- a/src/webui/static/style.css +++ b/src/webui/static/style.css @@ -524,7 +524,7 @@ header p { .info-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + grid-template-columns: 1fr; gap: 20px; }