mirror of
https://github.com/yggdrasil-network/yggdrasil-go.git
synced 2025-08-24 07:55:06 +03:00
Expose global state and update peer display logic in WebUI
This commit is contained in:
parent
675e2e71a5
commit
2b3b4c39d2
6 changed files with 312 additions and 162 deletions
|
@ -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 = '<div class="no-data">No peers connected</div>';
|
||||
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
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
<title>Yggdrasil Web Interface</title>
|
||||
<link rel="stylesheet" href="static/style.css">
|
||||
<script src="static/api.js"></script>
|
||||
<script src="static/main.js"></script>
|
||||
<script src="static/app.js"></script>
|
||||
<script src="static/lang/ru.js"></script>
|
||||
<script srс="static/lang/en.js"></script>
|
||||
<script src="static/lang/en.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -145,152 +146,7 @@
|
|||
<!-- Notifications container -->
|
||||
<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})">×</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>
|
||||
|
||||
</html>
|
|
@ -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',
|
||||
|
|
|
@ -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': 'Предупреждение',
|
||||
|
|
266
src/webui/static/main.js
Normal file
266
src/webui/static/main.js
Normal 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})">×</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);
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue