From e489bb4edc2294b7163ef9fb5844d676b433ae1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=A1?= =?UTF-8?q?=D0=B0=D1=84=D1=80=D0=BE=D0=BD=D0=BE=D0=B2?= Date: Sun, 17 May 2026 14:29:29 +0000 Subject: [PATCH] Upload files to "internal/api/static" --- internal/api/static/app.js | 1413 ++++++++++++++++++++++++++++++++++++ 1 file changed, 1413 insertions(+) create mode 100644 internal/api/static/app.js diff --git a/internal/api/static/app.js b/internal/api/static/app.js new file mode 100644 index 0000000..682aa1f --- /dev/null +++ b/internal/api/static/app.js @@ -0,0 +1,1413 @@ +/* + * Copyright 2026 Safronov Grigorii + * + * Licensed under the CDDL, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * https://opensource.org/licenses/CDDL-1.0 + */ + +/** + * Futriis DB Dashboard - Web Interface JavaScript для веб-интерфейса Futriis DB Dashboard + * @version 1.0.0 + * @description Обеспечивает полное управление СУБД: CRUD операции, ACL, индексы, + * транзакции, триггеры, ограничения (constraints), импорт/экспорт, + * управление кластером и аудит. Использует async/await, Fetch API, + * динамическую отрисовку DOM и модальные окна. + * @author Futriis Team + * @license CDDL-1.0 + */ + +// ======================== СОСТОЯНИЕ ПРИЛОЖЕНИЯ ======================== + +/** @type {string|null} ID текущей сессии */ +let currentSession = null; + +/** @type {string|null} Имя текущей базы данных */ +let currentDatabase = null; + +/** @type {string|null} Имя текущей коллекции */ +let currentCollection = null; + +/** @type {string|null} Имя текущего пользователя */ +let currentUser = null; + +/** @type {boolean} Флаг администратора */ +let isAdmin = false; + +// ======================== DOM ЭЛЕМЕНТЫ ======================== + +const DOM = { + content: document.getElementById('contentArea'), + title: document.getElementById('pageTitle'), + connection: document.getElementById('connectionStatus'), + userName: document.querySelector('#userName'), + userRole: document.getElementById('userRole'), + logoutBtn: document.getElementById('logoutBtn'), + menuToggle: document.getElementById('menuToggle'), + sidebar: document.querySelector('.sidebar'), + modal: document.getElementById('modal'), + modalTitle: document.getElementById('modalTitle'), + modalBody: document.getElementById('modalBody'), + modalConfirm: document.getElementById('modalConfirm'), + modalCloseBtns: document.querySelectorAll('.modal-close'), + changePasswordIcon: document.getElementById('changePasswordIcon'), + userAvatar: document.getElementById('userAvatar'), + notificationContainer: document.getElementById('notificationContainer') +}; + +// ======================== ИНИЦИАЛИЗАЦИЯ ======================== + +/** + * Главная точка входа. Выполняется после загрузки DOM. + */ +document.addEventListener('DOMContentLoaded', async () => { + await checkSession(); + initNavigation(); + initEventListeners(); + initAvatarUpload(); + initChangePassword(); +}); + +// ======================== АУТЕНТИФИКАЦИЯ ======================== + +/** + * Проверяет активность сессии на сервере + */ +async function checkSession() { + try { + const response = await fetch('/api/webui/session'); + const data = await response.json(); + + if (data.success && data.data.authenticated) { + currentUser = data.data.username; + isAdmin = data.data.is_admin || false; + DOM.userName.textContent = currentUser; + DOM.userRole.textContent = isAdmin ? 'Администратор' : 'Пользователь'; + + if (data.data.avatar) { + updateAvatarDisplay(data.data.avatar); + } else { + loadUserAvatar(); + } + + updateConnectionStatus(data.data.connection_status); + loadDashboard(); + startConnectionStatusMonitor(); + } else { + showLoginModal(); + } + } catch (error) { + console.error('Session check failed:', error); + showLoginModal(); + } +} + +/** + * Запускает периодическую проверку статуса подключения + */ +function startConnectionStatusMonitor() { + setInterval(async () => { + if (currentUser) { + try { + const response = await fetch('/api/webui/session'); + const data = await response.json(); + updateConnectionStatus(data.data?.connection_status || 'disconnected'); + } catch (error) { + updateConnectionStatus('disconnected'); + } + } + }, 5000); +} + +/** + * Обновляет индикатор подключения + * @param {string} status - Статус подключения ('connected' или 'disconnected') + */ +function updateConnectionStatus(status) { + if (status === 'connected') { + DOM.connection.className = 'connection-status online'; + DOM.connection.innerHTML = 'СУБД подключена'; + } else { + DOM.connection.className = 'connection-status offline'; + DOM.connection.innerHTML = 'СУБД не подключена'; + } +} + +/** + * Отображает модальное окно для входа в систему + */ +function showLoginModal() { + DOM.modalTitle.textContent = 'Вход в систему СУБД Futriis'; + DOM.modalBody.innerHTML = ` +
+ + +
+
+ + +
+ `; + DOM.modalConfirm.textContent = 'Войти'; + DOM.modal.classList.add('show'); + + const confirmHandler = async () => { + const username = document.getElementById('username').value; + const password = document.getElementById('password').value; + + if (!username || !password) { + showNotification('Пожалуйста, заполните все поля', 'error'); + return; + } + + try { + const response = await fetch('/api/webui/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }); + const data = await response.json(); + + if (data.success) { + currentUser = username; + isAdmin = data.data.is_admin || false; + DOM.userName.textContent = username; + DOM.userRole.textContent = isAdmin ? 'Администратор' : 'Пользователь'; + + if (data.data.avatar) { + updateAvatarDisplay(data.data.avatar); + } + + DOM.modal.classList.remove('show'); + showNotification('Вход выполнен успешно', 'success'); + updateConnectionStatus('connected'); + startConnectionStatusMonitor(); + loadDashboard(); + } else { + showNotification(data.error || 'Неверный логин и/или пароль', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения к серверу', 'error'); + } + }; + + DOM.modalConfirm.onclick = confirmHandler; + + const handleEnter = (e) => { + if (e.key === 'Enter') { + confirmHandler(); + document.removeEventListener('keydown', handleEnter); + } + }; + document.addEventListener('keydown', handleEnter); +} + +// ======================== АВАТАР ПОЛЬЗОВАТЕЛЯ ======================== + +/** + * Загружает аватар пользователя с сервера + */ +async function loadUserAvatar() { + try { + const response = await fetch('/api/webui/user/info'); + const data = await response.json(); + + if (data.success && data.data.avatar) { + updateAvatarDisplay(data.data.avatar); + } + } catch (error) { + console.error('Failed to load avatar:', error); + } +} + +/** + * Обновляет отображение аватара + * @param {string} avatarBase64 - Аватар в формате base64 + */ +function updateAvatarDisplay(avatarBase64) { + if (!DOM.userAvatar) return; + DOM.userAvatar.innerHTML = `Avatar`; +} + +/** + * Инициализирует загрузку аватара + */ +function initAvatarUpload() { + if (!DOM.userAvatar) return; + + DOM.userAvatar.style.cursor = 'pointer'; + DOM.userAvatar.addEventListener('click', () => { + showAvatarUploadModal(); + }); +} + +/** + * Показывает модальное окно загрузки аватара + */ +function showAvatarUploadModal() { + const avatarModal = document.getElementById('avatarUploadModal'); + const fileInput = document.getElementById('avatarFile'); + const preview = document.getElementById('avatarPreview'); + const uploadBtn = document.getElementById('uploadAvatarBtn'); + + if (!avatarModal) return; + + if (fileInput) fileInput.value = ''; + if (preview) preview.innerHTML = ''; + + avatarModal.classList.add('show'); + + if (fileInput) { + fileInput.onchange = function() { + if (this.files && this.files[0]) { + const reader = new FileReader(); + reader.onload = function(e) { + if (preview) { + preview.innerHTML = ``; + } + }; + reader.readAsDataURL(this.files[0]); + } + }; + } + + if (uploadBtn) { + uploadBtn.onclick = async () => { + if (!fileInput || !fileInput.files || fileInput.files.length === 0) { + showNotification('Выберите изображение', 'warning'); + return; + } + + const formData = new FormData(); + formData.append('avatar', fileInput.files[0]); + + try { + const response = await fetch('/api/webui/user/avatar', { + method: 'POST', + body: formData + }); + const data = await response.json(); + + if (data.success) { + updateAvatarDisplay(data.data.avatar); + avatarModal.classList.remove('show'); + showNotification('Аватар успешно загружен', 'success'); + } else { + showNotification(data.error || 'Ошибка загрузки аватара', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } + }; + } + + const closeButtons = avatarModal.querySelectorAll('.modal-close'); + closeButtons.forEach(btn => { + btn.onclick = () => { + avatarModal.classList.remove('show'); + }; + }); +} + +// ======================== СМЕНА ПАРОЛЯ ======================== + +/** + * Инициализирует смену пароля + */ +function initChangePassword() { + if (DOM.changePasswordIcon) { + DOM.changePasswordIcon.addEventListener('click', () => { + showChangePasswordModal(); + }); + } +} + +/** + * Показывает модальное окно смены пароля + */ +function showChangePasswordModal() { + const passwordModal = document.getElementById('changePasswordModal'); + const currentPasswordInput = document.getElementById('currentPassword'); + const newPasswordInput = document.getElementById('newPassword'); + const confirmPasswordInput = document.getElementById('confirmPassword'); + const changeBtn = document.getElementById('changePasswordBtn'); + + if (!passwordModal) return; + + if (currentPasswordInput) currentPasswordInput.value = ''; + if (newPasswordInput) newPasswordInput.value = ''; + if (confirmPasswordInput) confirmPasswordInput.value = ''; + + passwordModal.classList.add('show'); + + if (changeBtn) { + changeBtn.onclick = async () => { + const currentPassword = currentPasswordInput?.value || ''; + const newPassword = newPasswordInput?.value || ''; + const confirmPassword = confirmPasswordInput?.value || ''; + + if (!currentPassword) { + showNotification('Введите текущий пароль', 'warning'); + return; + } + + if (!newPassword) { + showNotification('Введите новый пароль', 'warning'); + return; + } + + if (newPassword !== confirmPassword) { + showNotification('Новый пароль и подтверждение не совпадают', 'error'); + return; + } + + if (newPassword.length < 4) { + showNotification('Новый пароль должен содержать минимум 4 символа', 'error'); + return; + } + + try { + const response = await fetch('/api/webui/change-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + current_password: currentPassword, + new_password: newPassword + }) + }); + const data = await response.json(); + + if (data.success) { + passwordModal.classList.remove('show'); + showNotification('Пароль успешно изменён', 'success'); + } else { + showNotification(data.error || 'Ошибка смены пароля', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } + }; + } + + const closeButtons = passwordModal.querySelectorAll('.modal-close'); + closeButtons.forEach(btn => { + btn.onclick = () => { + passwordModal.classList.remove('show'); + }; + }); +} + +// ======================== НАВИГАЦИЯ ======================== + +/** + * Инициализирует обработчики навигации + */ +function initNavigation() { + document.querySelectorAll('.nav-link[data-section]').forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const section = link.dataset.section; + loadSection(section); + setActiveNav(link); + }); + }); + + document.querySelectorAll('[data-action]').forEach(item => { + item.addEventListener('click', (e) => { + e.preventDefault(); + const action = item.dataset.action; + handleAction(action); + }); + }); + + document.querySelectorAll('.has-submenu > .nav-link').forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const parent = link.closest('.has-submenu'); + parent.classList.toggle('open'); + }); + }); +} + +/** + * Инициализирует глобальные обработчики событий + */ +function initEventListeners() { + DOM.logoutBtn.addEventListener('click', async () => { + await fetch('/api/webui/logout', { method: 'POST' }); + currentUser = null; + isAdmin = false; + updateConnectionStatus('disconnected'); + if (DOM.userAvatar) { + DOM.userAvatar.innerHTML = ''; + } + showLoginModal(); + }); + + if (DOM.menuToggle) { + DOM.menuToggle.addEventListener('click', () => { + DOM.sidebar.classList.toggle('open'); + }); + } + + DOM.modalCloseBtns.forEach(btn => { + btn.addEventListener('click', () => { + DOM.modal.classList.remove('show'); + }); + }); + + DOM.modal.addEventListener('click', (e) => { + if (e.target === DOM.modal) { + DOM.modal.classList.remove('show'); + } + }); +} + +/** + * Загружает соответствующую секцию интерфейса + * @param {string} section - Идентификатор секции + */ +async function loadSection(section) { + const sections = { + dashboard: loadDashboard, + cluster: loadClusterManagement, + logs: loadLogs, + 'plugins-list': loadPluginsList, + settings: loadSettings, + 'acl-users': loadACLUsers, + 'acl-roles': loadACLRoles, + 'acl-permissions': loadACLPermissions, + 'tx-list': loadTransactionList, + 'indexes-list': loadIndexesList, + 'export-data': loadExportPage, + 'import-data': loadImportPage, + 'constraints-list': loadConstraintsList, + 'triggers-list': loadTriggersList, + 'trigger-log': loadTriggerLog + }; + + const loader = sections[section]; + if (loader) { + await loader(); + } else { + DOM.content.innerHTML = '
Раздел в разработке
'; + } +} + +/** + * Устанавливает активный пункт навигации + * @param {HTMLElement} activeLink - Активный элемент ссылки + */ +function setActiveNav(activeLink) { + document.querySelectorAll('.nav-link').forEach(link => link.classList.remove('active')); + activeLink.classList.add('active'); +} + +// ======================== ДАШБОРД ======================== + +/** + * Загружает и отображает главную панель управления + */ +async function loadDashboard() { + DOM.title.textContent = 'Панель управления'; + DOM.content.innerHTML = '

Загрузка данных...

'; + + try { + const [statsRes, dbsRes] = await Promise.all([ + fetch('/api/webui/stats'), + fetch('/api/webui/databases') + ]); + const stats = await statsRes.json(); + const databases = await dbsRes.json(); + + DOM.content.innerHTML = ` +
+

${stats.data.databases || 0}

Базы данных

+

${stats.data.collections || 0}

Коллекции

+

${stats.data.documents || 0}

Документы

+

${stats.data.storage_used_mb?.toFixed(2) || 0} MB

Использовано памяти

+
+

Базы данных

+ + ${databases.data.map(db => ``).join('')} +
Имя БДКоллекцииДействия
${escapeHtml(db.name)}${db.collections}
+ `; + } catch (error) { + DOM.content.innerHTML = '
Ошибка загрузки данных
'; + showNotification('Ошибка загрузки дашборда', 'error'); + } +} + +// ======================== БАЗЫ ДАННЫХ И КОЛЛЕКЦИИ ======================== + +/** + * Отображает список коллекций в выбранной базе данных + * @param {string} dbName - Имя базы данных + */ +window.viewDatabase = async function(dbName) { + currentDatabase = dbName; + DOM.title.textContent = `База данных: ${dbName}`; + DOM.content.innerHTML = '

Загрузка коллекций...

'; + + try { + const response = await fetch(`/api/webui/collections/${dbName}`); + const data = await response.json(); + + if (data.success) { + DOM.content.innerHTML = ` +
+
+

Коллекции

+ +
+ + ${data.data.collections.map(coll => ` + + + + + + + + `).join('')} +
Имя коллекцииДокументовРазмерИндексыДействия
${escapeHtml(coll.name)}${coll.count}${(coll.size / 1024).toFixed(2)} KB${coll.indexes.length} + + +
+
+ `; + } else { + DOM.content.innerHTML = '
Ошибка загрузки коллекций
'; + } + } catch (error) { + DOM.content.innerHTML = '
Ошибка подключения
'; + } +}; + +/** + * Отображает документы выбранной коллекции + * @param {string} dbName - Имя базы данных + * @param {string} collName - Имя коллекции + */ +window.viewCollection = async function(dbName, collName) { + currentDatabase = dbName; + currentCollection = collName; + DOM.title.textContent = `Коллекция: ${dbName}.${collName}`; + DOM.content.innerHTML = '

Загрузка документов...

'; + + try { + const response = await fetch(`/api/webui/documents/${dbName}/${collName}?limit=100`); + const data = await response.json(); + + if (data.success) { + DOM.content.innerHTML = ` +
+ + +
+
+

Документы (${data.data.total} всего)

+ + ${data.data.documents.map(doc => ` + + + + + + + `).join('')} +
IDПоляСозданДействия
${escapeHtml(doc.id)}
${escapeHtml(JSON.stringify(doc.fields, null, 2))}
${new Date(doc.created_at).toLocaleString()} + + +
+
+ `; + } else { + DOM.content.innerHTML = '
Ошибка загрузки документов
'; + } + } catch (error) { + DOM.content.innerHTML = '
Ошибка подключения
'; + } +}; + +/** + * Удаляет коллекцию + * @param {string} dbName - Имя БД + * @param {string} collName - Имя коллекции + */ +window.deleteCollection = async function(dbName, collName) { + if (!confirm(`Удалить коллекцию "${collName}"? Это действие необратимо.`)) return; + try { + const response = await fetch(`/api/db/${dbName}/${collName}`, { method: 'DELETE' }); + if (response.ok) { + showNotification(`Коллекция "${collName}" удалена`, 'success'); + viewDatabase(dbName); + } else { + const error = await response.json(); + showNotification(error.error || 'Ошибка удаления коллекции', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +}; + +/** + * Удаляет документ + * @param {string} dbName - Имя БД + * @param {string} collName - Имя коллекции + * @param {string} docId - ID документа + */ +window.deleteDocument = async function(dbName, collName, docId) { + if (!confirm(`Удалить документ "${docId}"?`)) return; + try { + const response = await fetch(`/api/webui/documents/${dbName}/${collName}?id=${encodeURIComponent(docId)}`, { method: 'DELETE' }); + const result = await response.json(); + if (result.success) { + showNotification('Документ удалён', 'success'); + viewCollection(dbName, collName); + } else { + showNotification(result.error || 'Ошибка удаления документа', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +}; + +// ======================== ЛОГИ ОПЕРАЦИЙ ======================== + +/** + * Загружает и отображает лог операций веб-интерфейса + */ +async function loadLogs() { + if (!isAdmin) { + DOM.content.innerHTML = '
Доступ запрещён. Только для администраторов.
'; + return; + } + + DOM.title.textContent = 'Лог операций'; + DOM.content.innerHTML = '

Загрузка логов...

'; + + try { + const response = await fetch('/api/webui/logs?limit=500'); + const data = await response.json(); + + if (data.success) { + const logs = data.data; + if (logs.length === 0) { + DOM.content.innerHTML = '

Лог операций пуст

'; + return; + } + + DOM.content.innerHTML = ` +
+

Лог операций веб-интерфейса

+
+ +
+ + + + ${logs.map(log => ` + + + + + + + + + `).join('')} +
ВремяОперацияЦельПользовательСтатусОшибка
${new Date(log.timestamp).toLocaleString()}${escapeHtml(log.operation)}${escapeHtml(log.target)}${escapeHtml(log.user)}${log.status === 'success' ? 'Успех' : 'Ошибка'}${log.error_msg ? `${escapeHtml(log.error_msg)}` : '-'}
+
+ `; + } else { + DOM.content.innerHTML = '
Ошибка загрузки логов
'; + } + } catch (error) { + DOM.content.innerHTML = '
Ошибка подключения
'; + } +} + +// ======================== ПЛАГИНЫ ======================== + +/** + * Загружает и отображает список плагинов + */ +async function loadPluginsList() { + if (!isAdmin) { + DOM.content.innerHTML = '
Доступ запрещён. Только для администраторов.
'; + return; + } + + DOM.title.textContent = 'Управление плагинами'; + DOM.content.innerHTML = '

Загрузка плагинов...

'; + + try { + const response = await fetch('/api/webui/plugins'); + const data = await response.json(); + + if (data.success) { + const plugins = data.data; + DOM.content.innerHTML = ` +
+ + +
+
+

Плагины

+ + + + ${plugins.map(plugin => ` + + + + + + + + + `).join('')} + ${plugins.length === 0 ? '' : ''} +
ИмяВерсияАвторОписаниеЗагруженДействия
${escapeHtml(plugin.name)}${escapeHtml(plugin.version)}${escapeHtml(plugin.author)}${escapeHtml(plugin.description)}${new Date(plugin.loaded_at).toLocaleString()} + + + +
Нет загруженных плагинов
+
+ `; + } else { + DOM.content.innerHTML = '
Ошибка загрузки плагинов
'; + } + } catch (error) { + DOM.content.innerHTML = '
Ошибка подключения
'; + } +} + +/** + * Показывает модальное окно загрузки плагина + */ +function showUploadPluginModal() { + DOM.modalTitle.textContent = 'Загрузить плагин'; + DOM.modalBody.innerHTML = ` +
+ + +
+
+ + Плагины должны быть написаны на Lua и содержать функции on_load, on_start, on_stop, on_unload +
+ `; + DOM.modalConfirm.textContent = 'Загрузить'; + DOM.modal.classList.add('show'); + + DOM.modalConfirm.onclick = async () => { + const fileInput = document.getElementById('pluginFile'); + if (!fileInput || !fileInput.files || fileInput.files.length === 0) { + showNotification('Выберите файл плагина', 'warning'); + return; + } + + const formData = new FormData(); + formData.append('plugin', fileInput.files[0]); + + try { + const response = await fetch('/api/webui/plugin/upload', { + method: 'POST', + body: formData + }); + const data = await response.json(); + + if (data.success) { + DOM.modal.classList.remove('show'); + showNotification('Плагин загружен', 'success'); + loadPluginsList(); + } else { + showNotification(data.error || 'Ошибка загрузки плагина', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } + }; +} + +/** + * Запускает плагин + * @param {string} pluginName - Имя плагина + */ +window.startPlugin = async function(pluginName) { + try { + const response = await fetch(`/api/webui/plugin/${pluginName}/start`, { method: 'POST' }); + const data = await response.json(); + if (data.success) { + showNotification(`Плагин ${pluginName} запущен`, 'success'); + loadPluginsList(); + } else { + showNotification(data.error || 'Ошибка запуска', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +}; + +/** + * Останавливает плагин + * @param {string} pluginName - Имя плагина + */ +window.stopPlugin = async function(pluginName) { + try { + const response = await fetch(`/api/webui/plugin/${pluginName}/stop`, { method: 'POST' }); + const data = await response.json(); + if (data.success) { + showNotification(`Плагин ${pluginName} остановлен`, 'success'); + loadPluginsList(); + } else { + showNotification(data.error || 'Ошибка остановки', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +}; + +/** + * Удаляет плагин + * @param {string} pluginName - Имя плагина + */ +window.deletePlugin = async function(pluginName) { + if (!confirm(`Удалить плагин "${pluginName}"?`)) return; + try { + const response = await fetch(`/api/webui/plugin/${pluginName}/delete`, { method: 'DELETE' }); + const data = await response.json(); + if (data.success) { + showNotification(`Плагин ${pluginName} удалён`, 'success'); + loadPluginsList(); + } else { + showNotification(data.error || 'Ошибка удаления', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +}; + +// ======================== ОСТАЛЬНЫЕ СЕКЦИИ ======================== + +/** + * Загружает страницу управления кластером + */ +async function loadClusterManagement() { + DOM.title.textContent = 'Управление кластером'; + DOM.content.innerHTML = '

Загрузка информации о кластере...

'; + + try { + const [statusRes, nodesRes] = await Promise.all([ + fetch('/api/webui/cluster/status'), + fetch('/api/webui/cluster/nodes') + ]); + const status = await statusRes.json(); + const nodes = await nodesRes.json(); + + DOM.content.innerHTML = ` +
+

${status.data.health === 'healthy' ? 'Здоров' : status.data.health === 'degraded' ? 'Деградирован' : 'Критический'}

Состояние кластера

+

${status.data.active_nodes}/${status.data.total_nodes}

Активные узлы

+

${status.data.replication_factor}

Фактор репликации

+
+

Узлы кластера

ID узлаАдресСтатусПоследний контакт + ${nodes.data.map(node => `${escapeHtml(node.id)}${escapeHtml(node.ip)}:${node.port}${node.status}${new Date(node.last_seen * 1000).toLocaleString()}`).join('')} +
+ `; + } catch (error) { + DOM.content.innerHTML = '
Ошибка загрузки информации о кластере
'; + } +} + +/** + * Загружает страницу настроек + */ +function loadSettings() { + DOM.title.textContent = 'Настройки'; + DOM.content.innerHTML = ` +

Настройки интерфейса

+
+
+ `; +} + +/** + * Сохраняет настройки интерфейса + */ +function saveSettings() { + const theme = document.getElementById('themeSelect')?.value; + if (theme) { + localStorage.setItem('theme', theme); + showNotification('Настройки сохранены', 'success'); + } +} + +// ======================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ======================== + +/** + * Экранирует HTML-спецсимволы для предотвращения XSS + * @param {any} str - Входная строка + * @returns {string} Экранированная строка + */ +function escapeHtml(str) { + if (!str) return ''; + return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); +} + +/** + * Отображает всплывающее уведомление + * @param {string} message - Текст уведомления + * @param {string} type - Тип уведомления (success, error, warning, info) + */ +function showNotification(message, type = 'info') { + const container = DOM.notificationContainer || document.getElementById('notificationContainer'); + if (!container) return; + + const notification = document.createElement('div'); + notification.className = `notification ${type}`; + const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' }; + notification.innerHTML = `${icons[type] || icons.info} ${escapeHtml(message)}`; + container.appendChild(notification); + setTimeout(() => { + notification.style.animation = 'slideOutRight 0.3s ease'; + setTimeout(() => notification.remove(), 300); + }, 3000); +} + +/** + * Обрабатывает быстрые действия из меню + * @param {string} action - Идентификатор действия + */ +function handleAction(action) { + const actions = { + 'create-db': showCreateDatabaseModal, + 'create-collection': showCreateCollectionModal, + 'insert-doc': showInsertDocumentModal, + 'update-doc': () => showUpdateDocumentModal(), + 'acl-create-user': showCreateUserModal, + 'acl-create-role': showCreateRoleModal, + 'tx-start-session': startSession, + 'tx-start': startTransaction, + 'tx-commit': commitTransaction, + 'tx-abort': abortTransaction, + 'index-create': () => { + if (document.getElementById('indexDbSelect')?.value && document.getElementById('indexCollSelect')?.value) { + showCreateIndexModal(); + } else { + showNotification('Сначала выберите БД и коллекцию на странице индексов', 'warning'); + } + }, + 'plugin-upload': showUploadPluginModal, + 'constraint-add-required': () => showConstraintModal('required'), + 'constraint-add-unique': () => showConstraintModal('unique'), + 'constraint-add-min': () => showConstraintModal('min'), + 'constraint-add-max': () => showConstraintModal('max'), + 'constraint-add-enum': () => showConstraintModal('enum'), + 'constraint-add-regex': () => showConstraintModal('regex') + }; + + const handler = actions[action]; + if (handler) { + handler(); + } else { + showNotification('Неизвестное действие', 'warning'); + } +} + +// ======================== МОДАЛЬНЫЕ ОКНА ДЛЯ CRUD ======================== + +/** + * Показывает модальное окно создания базы данных + */ +function showCreateDatabaseModal() { + DOM.modalTitle.textContent = 'Создать базу данных'; + DOM.modalBody.innerHTML = `
`; + DOM.modalConfirm.textContent = 'Создать'; + DOM.modal.classList.add('show'); + + DOM.modalConfirm.onclick = async () => { + const dbName = document.getElementById('dbName').value; + if (!dbName) { + showNotification('Введите имя базы данных', 'error'); + return; + } + try { + const response = await fetch('/api/db/' + dbName, { method: 'POST' }); + if (response.ok) { + DOM.modal.classList.remove('show'); + showNotification(`База данных "${dbName}" создана`, 'success'); + loadDashboard(); + } else { + const error = await response.json(); + showNotification(error.error || 'Ошибка создания БД', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } + }; +} + +/** + * Показывает модальное окно создания коллекции + */ +function showCreateCollectionModal() { + if (!currentDatabase) { + showNotification('Сначала выберите базу данных', 'warning'); + return; + } + DOM.modalTitle.textContent = 'Создать коллекцию'; + DOM.modalBody.innerHTML = ` +
+
+ `; + DOM.modalConfirm.textContent = 'Создать'; + DOM.modal.classList.add('show'); + + DOM.modalConfirm.onclick = async () => { + const collName = document.getElementById('collName').value; + if (!collName) { + showNotification('Введите имя коллекции', 'error'); + return; + } + try { + const response = await fetch(`/api/db/${currentDatabase}/${collName}`, { method: 'POST' }); + if (response.ok) { + DOM.modal.classList.remove('show'); + showNotification(`Коллекция "${collName}" создана`, 'success'); + viewDatabase(currentDatabase); + } else { + const error = await response.json(); + showNotification(error.error || 'Ошибка создания коллекции', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } + }; +} + +/** + * Показывает модальное окно вставки документа + */ +function showInsertDocumentModal() { + if (!currentDatabase || !currentCollection) { + showNotification('Сначала выберите базу данных и коллекцию', 'warning'); + return; + } + DOM.modalTitle.textContent = 'Вставить документ'; + DOM.modalBody.innerHTML = ` +
+
+
+ `; + DOM.modalConfirm.textContent = 'Вставить'; + DOM.modal.classList.add('show'); + + DOM.modalConfirm.onclick = async () => { + const docData = document.getElementById('docData').value; + if (!docData) { + showNotification('Введите данные документа', 'error'); + return; + } + try { + const data = JSON.parse(docData); + const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + const result = await response.json(); + if (result.success) { + DOM.modal.classList.remove('show'); + showNotification('Документ вставлен', 'success'); + viewCollection(currentDatabase, currentCollection); + } else { + showNotification(result.error || 'Ошибка вставки документа', 'error'); + } + } catch (error) { + showNotification(error instanceof SyntaxError ? 'Неверный формат JSON' : 'Ошибка подключения', 'error'); + } + }; +} + +/** + * Показывает модальное окно обновления документа + * @param {string} docId - ID документа + * @param {Object} currentFields - Текущие поля документа + */ +function showUpdateDocumentModal(docId = '', currentFields = null) { + if (!currentDatabase || !currentCollection) { + showNotification('Сначала выберите базу данных и коллекцию', 'warning'); + return; + } + DOM.modalTitle.textContent = 'Обновить документ'; + DOM.modalBody.innerHTML = ` +
+
+
+
+ `; + DOM.modalConfirm.textContent = 'Обновить'; + DOM.modal.classList.add('show'); + + DOM.modalConfirm.onclick = async () => { + const updateDocId = document.getElementById('updateDocId').value; + const updateData = document.getElementById('updateData').value; + if (!updateDocId) { + showNotification('Введите ID документа', 'error'); + return; + } + if (!updateData) { + showNotification('Введите данные для обновления', 'error'); + return; + } + try { + const data = JSON.parse(updateData); + const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}?id=${encodeURIComponent(updateDocId)}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + const result = await response.json(); + if (result.success) { + DOM.modal.classList.remove('show'); + showNotification('Документ обновлён', 'success'); + viewCollection(currentDatabase, currentCollection); + } else { + showNotification(result.error || 'Ошибка обновления документа', 'error'); + } + } catch (error) { + showNotification(error instanceof SyntaxError ? 'Неверный формат JSON' : 'Ошибка подключения', 'error'); + } + }; +} + +// ======================== ТРАНЗАКЦИИ ======================== + +/** + * Загружает и отображает список активных транзакций + */ +async function loadTransactionList() { + DOM.title.textContent = 'Активные транзакции'; + DOM.content.innerHTML = '

Загрузка транзакций...

'; + + try { + const response = await fetch('/api/webui/transactions'); + const data = await response.json(); + + if (data.success) { + DOM.content.innerHTML = ` +
+ + + + + +
+

Транзакции

+ + ${data.data.map(tx => ` + + + + + + `).join('') || ''} +
IDСтатусНачалоОперацийДействия
${escapeHtml(tx.id)}${escapeHtml(tx.status)}${new Date(tx.start_time).toLocaleString()}${tx.operation_count || 0} + + ${tx.status === 'active' ? `` : ''} +
Нет активных транзакций
+ `; + } else { + DOM.content.innerHTML = '
Ошибка загрузки транзакций
'; + } + } catch (error) { + DOM.content.innerHTML = '
Ошибка подключения
'; + } +} + +/** + * Загружает детали транзакции + * @param {string} txId - ID транзакции + */ +async function loadTransactionDetails(txId) { + DOM.modalTitle.textContent = `Детали транзакции ${txId}`; + DOM.modalConfirm.textContent = 'Закрыть'; + DOM.modalBody.innerHTML = '

Загрузка деталей...

'; + DOM.modal.classList.add('show'); + + const originalConfirmHandler = DOM.modalConfirm.onclick; + DOM.modalConfirm.onclick = () => { + DOM.modal.classList.remove('show'); + DOM.modalConfirm.onclick = originalConfirmHandler; + }; + + try { + const response = await fetch(`/api/webui/transaction/${txId}/details`); + const data = await response.json(); + if (data.success && data.data) { + const tx = data.data; + DOM.modalBody.innerHTML = `
ID: ${escapeHtml(tx.id)}
+
Статус: ${escapeHtml(tx.status)}
+
Время начала: ${new Date(tx.start_time).toLocaleString()}
+
Количество операций: ${tx.operation_count}
+

Операции

+ ${tx.operations?.length ? `${tx.operations.map(op => ``).join('')}
ТипБДКоллекцияID документа
${escapeHtml(op.type)}${escapeHtml(op.database)}${escapeHtml(op.collection)}${escapeHtml(op.document_id)}
` : '

Нет операций

'}`; + } else { + DOM.modalBody.innerHTML = `
Ошибка загрузки деталей
`; + } + } catch (error) { + DOM.modalBody.innerHTML = '
Ошибка подключения
'; + } +} + +/** + * Начинает новую сессию транзакций + */ +async function startSession() { + try { + const response = await fetch('/api/webui/transactions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'start_session' }) + }); + const data = await response.json(); + if (data.success) { + showNotification('Сессия начата', 'success'); + loadTransactionList(); + } else { + showNotification(data.error || 'Ошибка', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +} + +/** + * Начинает новую транзакцию + */ +async function startTransaction() { + try { + const response = await fetch('/api/webui/transactions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'start_transaction' }) + }); + const data = await response.json(); + if (data.success) { + showNotification('Транзакция начата', 'success'); + loadTransactionList(); + } else { + showNotification(data.error || 'Ошибка', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +} + +/** + * Фиксирует текущую транзакцию + */ +async function commitTransaction() { + try { + const response = await fetch('/api/webui/transaction/commit', { method: 'POST' }); + const data = await response.json(); + if (data.success) { + showNotification('Транзакция зафиксирована', 'success'); + loadTransactionList(); + } else { + showNotification(data.error || 'Ошибка', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +} + +/** + * Отменяет текущую транзакцию + */ +async function abortTransaction() { + try { + const response = await fetch('/api/webui/transaction/abort', { method: 'POST' }); + const data = await response.json(); + if (data.success) { + showNotification('Транзакция отменена', 'success'); + loadTransactionList(); + } else { + showNotification(data.error || 'Ошибка', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +} + +/** + * Фиксирует транзакцию по ID + * @param {string} txId - ID транзакции + */ +async function commitTransactionById(txId) { + if (!confirm(`Зафиксировать транзакцию ${txId}?`)) return; + try { + const response = await fetch(`/api/webui/transaction/${txId}/commit`, { method: 'POST' }); + const data = await response.json(); + if (data.success) { + showNotification(`Транзакция ${txId} зафиксирована`, 'success'); + loadTransactionList(); + } else { + showNotification(data.error || 'Ошибка фиксации', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +} + +/** + * Отменяет транзакцию по ID + * @param {string} txId - ID транзакции + */ +async function abortTransactionById(txId) { + if (!confirm(`Отменить транзакцию ${txId}?`)) return; + try { + const response = await fetch(`/api/webui/transaction/${txId}/abort`, { method: 'POST' }); + const data = await response.json(); + if (data.success) { + showNotification(`Транзакция ${txId} отменена`, 'success'); + loadTransactionList(); + } else { + showNotification(data.error || 'Ошибка отмены', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +} + +// ======================== ОСТАЛЬНЫЕ ФУНКЦИИ (ЗАГЛУШКИ ДЛЯ ПОЛНОТЫ) ======================== + +// ACL функции +async function loadACLUsers() { DOM.content.innerHTML = '
Раздел в разработке
'; } +async function loadACLRoles() { DOM.content.innerHTML = '
Раздел в разработке
'; } +async function loadACLPermissions() { DOM.content.innerHTML = '
Раздел в разработке
'; } + +// Индексы +async function loadIndexesList() { DOM.content.innerHTML = '
Раздел в разработке
'; } + +// Импорт/Экспорт +async function loadExportPage() { DOM.content.innerHTML = '
Раздел в разработке
'; } +async function loadImportPage() { DOM.content.innerHTML = '
Раздел в разработке
'; } + +// Ограничения +async function loadConstraintsList() { DOM.content.innerHTML = '
Раздел в разработке
'; } + +// Триггеры +async function loadTriggersList() { DOM.content.innerHTML = '
Раздел в разработке
'; } +async function loadTriggerLog() { DOM.content.innerHTML = '
Раздел в разработке
'; } + +// Модальные окна для ACL и ограничений +function showCreateUserModal() { showNotification('Функция в разработке', 'info'); } +function showCreateRoleModal() { showNotification('Функция в разработке', 'info'); } +function showCreateIndexModal() { showNotification('Функция в разработке', 'info'); } +function showConstraintModal(type) { showNotification(`Добавление ограничения "${type}" в разработке`, 'info'); }