From e904443abf5687efcca2683c269f3b2351adc3be 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: Fri, 18 Sep 2026 22:04:11 +0000 Subject: [PATCH] Delete internal/api/static/app.js --- internal/api/static/app.js | 1413 ------------------------------------ 1 file changed, 1413 deletions(-) delete mode 100644 internal/api/static/app.js diff --git a/internal/api/static/app.js b/internal/api/static/app.js deleted file mode 100644 index 682aa1f..0000000 --- a/internal/api/static/app.js +++ /dev/null @@ -1,1413 +0,0 @@ -/* - * 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'); }