fix: frontend-backend integration — 14 issues resolved
- nginx: add proxy routes for /settings/, /templates/, /ipam/, /logs/, /admin/, /health
- dashboard: fix data format mismatch (servers[] not nodes[]/vms[])
- dashboard: add WebSocket subscription to dashboard_metrics for real-time updates
- dashboard: connect activity feed to /logs/api/logs endpoint
- dashboard: reduce polling to 15s with WS as primary update source
- graphs: fix rrddata response unwrap (backend returns {data:[],timeframe:''})
- graphs: add i18n translations for chart titles
- console: add xterm.js CSS import for proper terminal rendering
- console: add i18n translations (connecting, reconnect)
- workspace: add switcher dropdown in TopBar
- workspace: add edit dialog in WorkspacesPage
- workspace: create Zustand store with localStorage persistence
- workspace: add X-Active-Workspace header to API client
- sidebar: replace generic icon with actual favicon.svg logo
- instances: translate RowActionMenu and InstanceDetailPage strings
- locales: add graphs, console, workspace, instance tab keys to en/ru
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,157 +0,0 @@
|
||||
/**
|
||||
* Client-side translations
|
||||
* Synchronized with backend i18n.py
|
||||
*/
|
||||
|
||||
const translations = {
|
||||
ru: {
|
||||
// Common
|
||||
loading: "Загрузка...",
|
||||
save: "Сохранить",
|
||||
cancel: "Отмена",
|
||||
delete: "Удалить",
|
||||
edit: "Редактировать",
|
||||
add: "Добавить",
|
||||
search: "Поиск",
|
||||
filter: "Фильтр",
|
||||
actions: "Действия",
|
||||
status: "Статус",
|
||||
name: "Название",
|
||||
description: "Описание",
|
||||
success: "Успешно",
|
||||
error: "Ошибка",
|
||||
warning: "Предупреждение",
|
||||
confirm: "Подтвердить",
|
||||
yes: "Да",
|
||||
no: "Нет",
|
||||
|
||||
// Messages
|
||||
profile_updated: "Профиль обновлён",
|
||||
password_changed: "Пароль успешно изменён",
|
||||
settings_saved: "Настройки сохранены",
|
||||
language_changed: "Язык изменён. Страница будет перезагружена...",
|
||||
error_occurred: "Произошла ошибка",
|
||||
confirm_delete: "Вы уверены, что хотите удалить?",
|
||||
|
||||
// Updates
|
||||
update_check_disabled: "Проверка обновлений отключена",
|
||||
checking_updates: "Проверка обновлений...",
|
||||
check_updates: "Проверить обновления",
|
||||
update_available: "Доступно обновление",
|
||||
no_updates: "Установлена последняя версия",
|
||||
update_warning: "Панель будет недоступна во время обновления",
|
||||
updating_system: "Обновление системы...",
|
||||
|
||||
// SSH Keys
|
||||
ssh_key_saved: "SSH ключ успешно сохранён",
|
||||
error_saving_ssh_key: "Ошибка сохранения SSH ключа",
|
||||
invalid_ssh_key_format: "Неверный формат SSH ключа",
|
||||
|
||||
// Dashboard
|
||||
nodes_label: "Ноды",
|
||||
clusters_label: "Кластеры",
|
||||
alerts_label: "Оповещения",
|
||||
vms: "ВМ",
|
||||
running: "Запущено",
|
||||
online: "Онлайн",
|
||||
paused: "Пауза",
|
||||
stopped: "Остановлен",
|
||||
loading_dots: "Загрузка…",
|
||||
no_vms_found: "Нет виртуальных машин",
|
||||
no_nodes: "Нет доступных нод",
|
||||
all_healthy: "Все работают",
|
||||
no_alerts: "Нет оповещений",
|
||||
},
|
||||
|
||||
en: {
|
||||
// Common
|
||||
loading: "Loading...",
|
||||
save: "Save",
|
||||
cancel: "Cancel",
|
||||
delete: "Delete",
|
||||
edit: "Edit",
|
||||
add: "Add",
|
||||
search: "Search",
|
||||
filter: "Filter",
|
||||
actions: "Actions",
|
||||
status: "Status",
|
||||
name: "Name",
|
||||
description: "Description",
|
||||
success: "Success",
|
||||
error: "Error",
|
||||
warning: "Warning",
|
||||
confirm: "Confirm",
|
||||
yes: "Yes",
|
||||
no: "No",
|
||||
|
||||
// Messages
|
||||
profile_updated: "Profile updated",
|
||||
password_changed: "Password changed successfully",
|
||||
settings_saved: "Settings saved",
|
||||
language_changed: "Language changed. Page will reload...",
|
||||
error_occurred: "An error occurred",
|
||||
confirm_delete: "Are you sure you want to delete?",
|
||||
|
||||
// Updates
|
||||
update_check_disabled: "Update check is disabled",
|
||||
checking_updates: "Checking for updates...",
|
||||
check_updates: "Check for Updates",
|
||||
update_available: "Update available",
|
||||
no_updates: "You have the latest version",
|
||||
update_warning: "Panel will be unavailable during update",
|
||||
updating_system: "Updating system...",
|
||||
|
||||
// SSH Keys
|
||||
ssh_key_saved: "SSH key saved successfully",
|
||||
error_saving_ssh_key: "Error saving SSH key",
|
||||
invalid_ssh_key_format: "Invalid SSH key format",
|
||||
|
||||
// Dashboard
|
||||
nodes_label: "Nodes",
|
||||
clusters_label: "Clusters",
|
||||
alerts_label: "Alerts",
|
||||
vms: "VMs",
|
||||
running: "Running",
|
||||
online: "Online",
|
||||
paused: "Paused",
|
||||
stopped: "Stopped",
|
||||
loading_dots: "Loading…",
|
||||
no_vms_found: "No VMs found",
|
||||
no_nodes: "No nodes available",
|
||||
all_healthy: "All healthy",
|
||||
no_alerts: "No alerts",
|
||||
}
|
||||
};
|
||||
|
||||
// Get current language from localStorage or default
|
||||
function getCurrentLanguage() {
|
||||
return localStorage.getItem('panel_language') || 'ru';
|
||||
}
|
||||
|
||||
// Set language
|
||||
function setLanguage(lang) {
|
||||
localStorage.setItem('panel_language', lang);
|
||||
}
|
||||
|
||||
// Get translation
|
||||
function t(key, lang = null) {
|
||||
if (!lang) {
|
||||
lang = getCurrentLanguage();
|
||||
}
|
||||
|
||||
if (translations[lang] && translations[lang][key]) {
|
||||
return translations[lang][key];
|
||||
}
|
||||
|
||||
// Fallback to Russian
|
||||
if (translations.ru[key]) {
|
||||
return translations.ru[key];
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
// Export for use in other scripts
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { translations, t, getCurrentLanguage, setLanguage };
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
/**
|
||||
* Theme Manager - Handles light/dark mode switching and sidebar toggle
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const THEME_KEY = 'panel-theme';
|
||||
const SIDEBAR_KEY = 'panel-sidebar';
|
||||
const DARK_THEME = 'dark';
|
||||
const LIGHT_THEME = 'light';
|
||||
|
||||
// Get saved theme or default to light
|
||||
function getPreferredTheme() {
|
||||
const savedTheme = localStorage.getItem(THEME_KEY);
|
||||
if (savedTheme) {
|
||||
return savedTheme;
|
||||
}
|
||||
|
||||
// Default to light theme (cloud panel style)
|
||||
return LIGHT_THEME;
|
||||
}
|
||||
|
||||
// Apply theme to document
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem(THEME_KEY, theme);
|
||||
|
||||
// Update toggle button icon
|
||||
updateToggleIcon(theme);
|
||||
}
|
||||
|
||||
// Update toggle button icon
|
||||
function updateToggleIcon(theme) {
|
||||
const toggle = document.getElementById('themeToggle');
|
||||
if (!toggle) return;
|
||||
|
||||
const sunIcon = toggle.querySelector('.icon-sun');
|
||||
const moonIcon = toggle.querySelector('.icon-moon');
|
||||
|
||||
if (sunIcon && moonIcon) {
|
||||
if (theme === DARK_THEME) {
|
||||
sunIcon.style.display = 'block';
|
||||
moonIcon.style.display = 'none';
|
||||
} else {
|
||||
sunIcon.style.display = 'none';
|
||||
moonIcon.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle theme
|
||||
function toggleTheme() {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme') || DARK_THEME;
|
||||
const newTheme = currentTheme === DARK_THEME ? LIGHT_THEME : DARK_THEME;
|
||||
applyTheme(newTheme);
|
||||
}
|
||||
|
||||
// Toggle sidebar
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
|
||||
if (sidebar) {
|
||||
sidebar.classList.toggle('open');
|
||||
|
||||
if (overlay) {
|
||||
overlay.classList.toggle('active', sidebar.classList.contains('open'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close sidebar on escape key
|
||||
function handleKeydown(e) {
|
||||
if (e.key === 'Escape') {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
|
||||
if (sidebar && sidebar.classList.contains('open')) {
|
||||
sidebar.classList.remove('open');
|
||||
if (overlay) {
|
||||
overlay.classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on DOM load
|
||||
function init() {
|
||||
// Apply saved or preferred theme immediately
|
||||
const theme = getPreferredTheme();
|
||||
applyTheme(theme);
|
||||
|
||||
// Set up toggle button
|
||||
const toggle = document.getElementById('themeToggle');
|
||||
if (toggle) {
|
||||
toggle.addEventListener('click', toggleTheme);
|
||||
}
|
||||
|
||||
// Listen for escape key
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
|
||||
// Listen for system theme changes
|
||||
if (window.matchMedia) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
// Only auto-switch if user hasn't manually selected a theme
|
||||
if (!localStorage.getItem(THEME_KEY)) {
|
||||
applyTheme(e.matches ? DARK_THEME : LIGHT_THEME);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close sidebar when clicking on a link (mobile)
|
||||
const sidebarLinks = document.querySelectorAll('.sidebar-nav a');
|
||||
sidebarLinks.forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
|
||||
if (window.innerWidth <= 768 && sidebar) {
|
||||
sidebar.classList.remove('open');
|
||||
if (overlay) {
|
||||
overlay.classList.remove('active');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Apply theme before DOM is ready to prevent flash
|
||||
const savedTheme = getPreferredTheme();
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// Expose functions globally
|
||||
window.toggleTheme = toggleTheme;
|
||||
window.toggleSidebar = toggleSidebar;
|
||||
})();
|
||||
Reference in New Issue
Block a user