Expose global state and update peer display logic in WebUI

This commit is contained in:
Andy Oknen 2025-07-30 16:09:40 +00:00
parent 675e2e71a5
commit 2b3b4c39d2
6 changed files with 312 additions and 162 deletions

View file

@ -3,9 +3,9 @@
* Integrates admin API with the user interface * Integrates admin API with the user interface
*/ */
// Global state // Global state - expose to window for access from other scripts
let nodeInfo = null; window.nodeInfo = null;
let peersData = null; window.peersData = null;
let isLoading = false; let isLoading = false;
/** /**
@ -14,7 +14,7 @@ let isLoading = false;
async function loadNodeInfo() { async function loadNodeInfo() {
try { try {
const info = await window.yggAPI.getSelf(); const info = await window.yggAPI.getSelf();
nodeInfo = info; window.nodeInfo = info;
updateNodeInfoDisplay(info); updateNodeInfoDisplay(info);
return info; return info;
} catch (error) { } catch (error) {
@ -30,7 +30,7 @@ async function loadNodeInfo() {
async function loadPeers() { async function loadPeers() {
try { try {
const data = await window.yggAPI.getPeers(); const data = await window.yggAPI.getPeers();
peersData = data; window.peersData = data;
updatePeersDisplay(data); updatePeersDisplay(data);
return data; return data;
} catch (error) { } catch (error) {
@ -64,6 +64,13 @@ function updatePeersDisplay(data) {
peersContainer.innerHTML = ''; 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) { if (!data.peers || data.peers.length === 0) {
peersContainer.innerHTML = '<div class="no-data">No peers connected</div>'; peersContainer.innerHTML = '<div class="no-data">No peers connected</div>';
return; return;
@ -73,12 +80,12 @@ function updatePeersDisplay(data) {
const peerElement = createPeerElement(peer); const peerElement = createPeerElement(peer);
peersContainer.appendChild(peerElement); 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 * Create HTML element for a single peer
*/ */
@ -232,6 +239,11 @@ async function initializeApp() {
} }
isLoading = true; isLoading = true;
// Initialize peer counts to 0 immediately to replace "Loading..." text
updateElementText('peers-count', '0');
updateElementText('peers-online', '0');
showInfo('Loading dashboard...'); showInfo('Loading dashboard...');
// Load initial data // Load initial data

View file

@ -7,9 +7,10 @@
<title>Yggdrasil Web Interface</title> <title>Yggdrasil Web Interface</title>
<link rel="stylesheet" href="static/style.css"> <link rel="stylesheet" href="static/style.css">
<script src="static/api.js"></script> <script src="static/api.js"></script>
<script src="static/main.js"></script>
<script src="static/app.js"></script> <script src="static/app.js"></script>
<script src="static/lang/ru.js"></script> <script src="static/lang/ru.js"></script>
<script srс="static/lang/en.js"></script> <script src="static/lang/en.js"></script>
</head> </head>
<body> <body>
@ -145,152 +146,7 @@
<!-- Notifications container --> <!-- Notifications container -->
<div class="notifications-container" id="notifications-container"></div> <div class="notifications-container" id="notifications-container"></div>
<script>
let currentLanguage = localStorage.getItem('yggdrasil-language') || 'ru';
let currentTheme = localStorage.getItem('yggdrasil-theme') || 'light';
function updateTexts() {
const elements = document.querySelectorAll('[data-key]');
elements.forEach(element => {
const key = element.getAttribute('data-key');
if (window.translations && window.translations[currentLanguage] && window.translations[currentLanguage][key]) {
element.textContent = window.translations[currentLanguage][key];
}
});
}
function toggleTheme() {
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
applyTheme();
localStorage.setItem('yggdrasil-theme', currentTheme);
}
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' ? '🌙' : '☀️';
}
}
}
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();
}
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
event.target.closest('.nav-item').classList.add('active');
}
// Notification system (shared with app.js)
let notificationId = 0;
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 = `
<div class="notification-icon">${icons[type] || icons.info}</div>
<div class="notification-content">
<div class="notification-title">${title || titles[type]}</div>
<div class="notification-message">${message}</div>
</div>
<button class="notification-close" onclick="removeNotification(${id})">&times;</button>
`;
container.appendChild(notification);
// Auto remove after duration
if (duration > 0) {
setTimeout(() => {
removeNotification(id);
}, duration);
}
return id;
}
function removeNotification(id) {
const notification = document.getElementById(`notification-${id}`);
if (notification) {
notification.classList.add('removing');
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}
}
function showSuccess(message, title = null) {
return showNotification(message, 'success', title);
}
function showError(message, title = null) {
return showNotification(message, 'error', title);
}
function showWarning(message, title = null) {
return showNotification(message, 'warning', title);
}
function showInfo(message, title = null) {
return showNotification(message, 'info', title);
}
// Initialize language and theme on page load
document.addEventListener('DOMContentLoaded', function () {
// Set active language button
document.getElementById('lang-' + currentLanguage).classList.add('active');
// Update all texts
updateTexts();
// Apply saved theme
applyTheme();
});
</script>
</body> </body>
</html> </html>

View file

@ -8,19 +8,28 @@ window.translations.en = {
'nav_config': 'Configuration', 'nav_config': 'Configuration',
'status_title': 'Node Status', 'status_title': 'Node Status',
'status_active': 'Active', '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', 'network_info': 'Network Information',
'address': 'Address', 'address': 'Address',
'subnet': 'Subnet', 'subnet': 'Subnet',
'statistics': 'Statistics', 'statistics': 'Peer Statistics',
'total_peers': 'Total Peers',
'online_peers': 'Online Peers',
'uptime': 'Uptime', 'uptime': 'Uptime',
'connections': 'Active connections', 'connections': 'Active connections',
'peers_title': 'Peer Management', 'peers_title': 'Peer Management',
'peers_description': 'View and manage peer connections', 'peers_description': 'View and manage peer connections',
'connected_peers': 'Connected Peers',
'active_peers': 'Active Peers', 'active_peers': 'Active Peers',
'active_connections': 'Number of active connections', 'active_connections': 'Number of active connections',
'add_peer': 'Add Peer', 'add_peer': 'Add Peer',
'add_peer_description': 'Connect to a new node', 'add_peer_description': 'Connect to a new node',
'add_peer_btn': 'Add Peer',
'config_title': 'Configuration', 'config_title': 'Configuration',
'config_description': 'Node settings and network parameters', 'config_description': 'Node settings and network parameters',
'basic_settings': 'Basic Settings', 'basic_settings': 'Basic Settings',
@ -37,7 +46,6 @@ window.translations.en = {
'access_dashboard': 'Access Dashboard', 'access_dashboard': 'Access Dashboard',
'error_invalid_password': 'Invalid password. Please try again.', 'error_invalid_password': 'Invalid password. Please try again.',
'error_too_many_attempts': 'Too many failed attempts. Please wait 1 minute before trying 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_success': 'Success',
'notification_error': 'Error', 'notification_error': 'Error',
'notification_warning': 'Warning', 'notification_warning': 'Warning',

View file

@ -8,19 +8,28 @@ window.translations.ru = {
'nav_config': 'Конфигурация', 'nav_config': 'Конфигурация',
'status_title': 'Состояние узла', 'status_title': 'Состояние узла',
'status_active': 'Активен', 'status_active': 'Активен',
'status_description': 'WebUI запущен и доступен', 'status_description': 'Информация о текущем состоянии вашего узла Yggdrasil',
'node_info': 'Информация об узле',
'public_key': 'Публичный ключ',
'version': 'Версия',
'routing_entries': 'Записей маршрутизации',
'loading': 'Загрузка...',
'network_info': 'Сетевая информация', 'network_info': 'Сетевая информация',
'address': 'Адрес', 'address': 'Адрес',
'subnet': 'Подсеть', 'subnet': 'Подсеть',
'statistics': 'Статистика', 'statistics': 'Статистика пиров',
'total_peers': 'Всего пиров',
'online_peers': 'Онлайн пиров',
'uptime': 'Время работы', 'uptime': 'Время работы',
'connections': 'Активных соединений', 'connections': 'Активных соединений',
'peers_title': 'Управление пирами', 'peers_title': 'Управление пирами',
'peers_description': 'Просмотр и управление соединениями с пирами', 'peers_description': 'Просмотр и управление соединениями с пирами',
'connected_peers': 'Подключенные пиры',
'active_peers': 'Активные пиры', 'active_peers': 'Активные пиры',
'active_connections': 'Количество активных соединений', 'active_connections': 'Количество активных соединений',
'add_peer': 'Добавить пир', 'add_peer': 'Добавить пир',
'add_peer_description': 'Подключение к новому узлу', 'add_peer_description': 'Подключение к новому узлу',
'add_peer_btn': 'Добавить пир',
'config_title': 'Конфигурация', 'config_title': 'Конфигурация',
'config_description': 'Настройки узла и параметры сети', 'config_description': 'Настройки узла и параметры сети',
'basic_settings': 'Основные настройки', 'basic_settings': 'Основные настройки',
@ -37,7 +46,6 @@ window.translations.ru = {
'access_dashboard': 'Войти в панель', 'access_dashboard': 'Войти в панель',
'error_invalid_password': 'Неверный пароль. Попробуйте снова.', 'error_invalid_password': 'Неверный пароль. Попробуйте снова.',
'error_too_many_attempts': 'Слишком много неудачных попыток. Подождите 1 минуту перед повторной попыткой.', 'error_too_many_attempts': 'Слишком много неудачных попыток. Подождите 1 минуту перед повторной попыткой.',
'add_peer_btn': 'Добавить пир',
'notification_success': 'Успешно', 'notification_success': 'Успешно',
'notification_error': 'Ошибка', 'notification_error': 'Ошибка',
'notification_warning': 'Предупреждение', 'notification_warning': 'Предупреждение',

266
src/webui/static/main.js Normal file
View file

@ -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 = `
<div class="notification-icon">${icons[type] || icons.info}</div>
<div class="notification-content">
<div class="notification-title">${title || titles[type]}</div>
<div class="notification-message">${message}</div>
</div>
<button class="notification-close" onclick="removeNotification(${id})">&times;</button>
`;
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);

View file

@ -524,7 +524,7 @@ header p {
.info-grid { .info-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-template-columns: 1fr;
gap: 20px; gap: 20px;
} }