/* * 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'); }