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 Network • ',
+ '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);