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 = `
`;
+}
+
+/**
+ * Инициализирует загрузку аватара
+ */
+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 => `| ${escapeHtml(db.name)} | ${db.collections} | |
`).join('')}
+
+ `;
+ } 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 => `
+
+ | ${escapeHtml(coll.name)} |
+ ${coll.count} |
+ ${(coll.size / 1024).toFixed(2)} KB |
+ ${coll.indexes.length} |
+
+
+
+ |
+
+ `).join('')}
+
+
+ `;
+ } 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} всего)
+
| ID | Поля | Создан | Действия |
+ ${data.data.documents.map(doc => `
+
+ ${escapeHtml(doc.id)} |
+ ${escapeHtml(JSON.stringify(doc.fields, null, 2))} |
+ ${new Date(doc.created_at).toLocaleString()} |
+
+
+
+ |
+
+ `).join('')}
+
+
+ `;
+ } 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 => `
+
+ | ${new Date(log.timestamp).toLocaleString()} |
+ ${escapeHtml(log.operation)} |
+ ${escapeHtml(log.target)} |
+ ${escapeHtml(log.user)} |
+ ${log.status === 'success' ? 'Успех' : 'Ошибка'} |
+ ${log.error_msg ? `${escapeHtml(log.error_msg)}` : '-'} |
+
+ `).join('')}
+
+
+ `;
+ } 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 => `
+
+ | ${escapeHtml(plugin.name)} |
+ ${escapeHtml(plugin.version)} |
+ ${escapeHtml(plugin.author)} |
+ ${escapeHtml(plugin.description)} |
+ ${new Date(plugin.loaded_at).toLocaleString()} |
+
+
+
+
+ |
+
+ `).join('')}
+ ${plugins.length === 0 ? '| Нет загруженных плагинов |
' : ''}
+
+
+ `;
+ } 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 = `
+
+
+
+
+
+
+
+ Транзакции
+
| ID | Статус | Начало | Операций | Действия |
+ ${data.data.map(tx => `
+ ${escapeHtml(tx.id)} |
+ ${escapeHtml(tx.status)} |
+ ${new Date(tx.start_time).toLocaleString()} |
+ ${tx.operation_count || 0} |
+
+
+ ${tx.status === 'active' ? `` : ''}
+ |
+
`).join('') || '| Нет активных транзакций |
'}
+
+ `;
+ } 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 ? `| Тип | БД | Коллекция | ID документа |
${tx.operations.map(op => `| ${escapeHtml(op.type)} | ${escapeHtml(op.database)} | ${escapeHtml(op.collection)} | ${escapeHtml(op.document_id)} |
`).join('')}
` : 'Нет операций
'}`;
+ } 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'); }