diff --git a/internal/api/static/app.js b/internal/api/static/app.js
new file mode 100644
index 0000000..ad6ffca
--- /dev/null
+++ b/internal/api/static/app.js
@@ -0,0 +1,2553 @@
+/*
+ * 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
+ */
+
+// Файл: internal/api/static/app.js
+// JavaScript для веб-интерфейса Futriis DB Dashboard
+
+// Глобальное состояние
+let currentSession = null;
+let currentDatabase = null;
+let currentCollection = null;
+let currentUser = null;
+
+// DOM элементы
+const contentArea = document.getElementById('contentArea');
+const pageTitle = document.getElementById('pageTitle');
+const connectionStatus = document.getElementById('connectionStatus');
+const userInfoSpan = document.querySelector('#userInfo span');
+const logoutBtn = document.getElementById('logoutBtn');
+const menuToggle = document.getElementById('menuToggle');
+const sidebar = document.querySelector('.sidebar');
+const modal = document.getElementById('modal');
+const modalTitle = document.getElementById('modalTitle');
+const modalBody = document.getElementById('modalBody');
+const modalConfirm = document.getElementById('modalConfirm');
+const modalCloseBtns = document.querySelectorAll('.modal-close');
+
+// Инициализация приложения
+document.addEventListener('DOMContentLoaded', () => {
+ checkSession();
+ initNavigation();
+ initEventListeners();
+});
+
+// Проверка сессии
+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;
+ userInfoSpan.textContent = currentUser;
+
+ if (data.data.connection_status === 'connected') {
+ connectionStatus.className = 'connection-status online';
+ connectionStatus.innerHTML = 'СУБД подключена';
+ } else {
+ connectionStatus.className = 'connection-status offline';
+ connectionStatus.innerHTML = 'СУБД не подключена';
+ }
+
+ loadDashboard();
+ } 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();
+
+ if (data.success && data.data.connection_status === 'connected') {
+ connectionStatus.className = 'connection-status online';
+ connectionStatus.innerHTML = 'СУБД подключена';
+ } else {
+ connectionStatus.className = 'connection-status offline';
+ connectionStatus.innerHTML = 'СУБД не подключена';
+ }
+ } catch (error) {
+ connectionStatus.className = 'connection-status offline';
+ connectionStatus.innerHTML = 'СУБД не подключена';
+ }
+ }
+ }, 5000);
+}
+
+// Показать модальное окно входа
+function showLoginModal() {
+ modalTitle.textContent = 'Вход в систему субд Futriis';
+ modalBody.innerHTML = `
+
+
+
+
+
+
+
+
+ `;
+
+ // Меняем текст на кнопке "Подтвердить" на "Войти"
+ modalConfirm.textContent = 'Войти';
+
+ 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;
+ userInfoSpan.textContent = username;
+ modal.classList.remove('show');
+ showNotification('Вход выполнен успешно', 'success');
+ connectionStatus.className = 'connection-status online';
+ connectionStatus.innerHTML = 'СУБД подключена';
+ startConnectionStatusMonitor();
+ loadDashboard();
+ } else {
+ showNotification(data.error || 'Неверный логин и/или пароль', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения к серверу', 'error');
+ }
+ };
+
+ modalConfirm.onclick = confirmHandler;
+
+ // Обработка Enter
+ const handleEnter = (e) => {
+ if (e.key === 'Enter') {
+ confirmHandler();
+ document.removeEventListener('keydown', handleEnter);
+ }
+ };
+ document.addEventListener('keydown', handleEnter);
+}
+
+// Инициализация навигации
+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;
+ handleCrudAction(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() {
+ logoutBtn.addEventListener('click', async () => {
+ await fetch('/api/webui/logout', { method: 'POST' });
+ currentSession = null;
+ currentUser = null;
+ connectionStatus.className = 'connection-status offline';
+ connectionStatus.innerHTML = 'СУБД не подключена';
+ showLoginModal();
+ });
+
+ if (menuToggle) {
+ menuToggle.addEventListener('click', () => {
+ sidebar.classList.toggle('open');
+ });
+ }
+
+ modalCloseBtns.forEach(btn => {
+ btn.addEventListener('click', () => {
+ modal.classList.remove('show');
+ });
+ });
+
+ modal.addEventListener('click', (e) => {
+ if (e.target === modal) {
+ modal.classList.remove('show');
+ }
+ });
+}
+
+// Загрузка секции
+async function loadSection(section) {
+ switch(section) {
+ case 'dashboard':
+ loadDashboard();
+ break;
+ case 'cluster':
+ loadClusterManagement();
+ break;
+ case 'audit':
+ loadAuditLog();
+ break;
+ case 'settings':
+ loadSettings();
+ break;
+ case 'acl-users':
+ loadACLUsers();
+ break;
+ case 'acl-roles':
+ loadACLRoles();
+ break;
+ case 'acl-permissions':
+ loadACLPermissions();
+ break;
+ case 'tx-list':
+ loadTransactionList();
+ break;
+ case 'indexes-list':
+ loadIndexesList();
+ break;
+ case 'export-data':
+ loadExportPage();
+ break;
+ case 'import-data':
+ loadImportPage();
+ break;
+ default:
+ loadDashboard();
+ }
+}
+
+// Загрузка дашборда
+async function loadDashboard() {
+ pageTitle.textContent = 'Панель управления';
+ contentArea.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();
+
+ contentArea.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) {
+ contentArea.innerHTML = 'Ошибка загрузки данных
';
+ showNotification('Ошибка загрузки дашборда', 'error');
+ }
+}
+
+// Просмотр базы данных
+window.viewDatabase = async function(dbName) {
+ currentDatabase = dbName;
+ pageTitle.textContent = `База данных: ${dbName}`;
+ contentArea.innerHTML = '';
+
+ try {
+ const response = await fetch(`/api/webui/collections/${dbName}`);
+ const data = await response.json();
+
+ if (data.success) {
+ contentArea.innerHTML = `
+
+
+
Коллекции
+
+
+
+
+ | Имя коллекции | Документов | Размер | Индексы | Действия |
+
+
+ ${data.data.collections.map(coll => `
+
+ | ${escapeHtml(coll.name)} |
+ ${coll.count} |
+ ${(coll.size / 1024).toFixed(2)} KB |
+ ${coll.indexes.length} |
+
+
+
+ |
+
+ `).join('')}
+
+
+
+ `;
+ } else {
+ contentArea.innerHTML = 'Ошибка загрузки коллекций
';
+ }
+ } catch (error) {
+ contentArea.innerHTML = 'Ошибка подключения
';
+ }
+};
+
+// Просмотр коллекции
+window.viewCollection = async function(dbName, collName) {
+ currentDatabase = dbName;
+ currentCollection = collName;
+ pageTitle.textContent = `Коллекция: ${dbName}.${collName}`;
+ contentArea.innerHTML = '';
+
+ try {
+ const response = await fetch(`/api/webui/documents/${dbName}/${collName}?limit=100`);
+ const data = await response.json();
+
+ if (data.success) {
+ contentArea.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 {
+ contentArea.innerHTML = 'Ошибка загрузки документов
';
+ }
+ } catch (error) {
+ contentArea.innerHTML = 'Ошибка подключения
';
+ }
+};
+
+// Загрузка управления кластером
+async function loadClusterManagement() {
+ pageTitle.textContent = 'Управление кластером';
+ contentArea.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();
+
+ contentArea.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) {
+ contentArea.innerHTML = 'Ошибка загрузки информации о кластере
';
+ }
+}
+
+// Загрузка лога аудита
+async function loadAuditLog() {
+ pageTitle.textContent = 'Лог аудита';
+ contentArea.innerHTML = '';
+ contentArea.innerHTML = 'Функция в разработке
';
+}
+
+// Загрузка настроек
+function loadSettings() {
+ pageTitle.textContent = 'Настройки';
+ contentArea.innerHTML = `
+
+
Настройки интерфейса
+
+
+
+
+
+
+ `;
+}
+
+// ==================== ACL Functions ====================
+
+// Загрузка списка пользователей
+async function loadACLUsers() {
+ pageTitle.textContent = 'Управление пользователями ACL';
+ contentArea.innerHTML = 'Загрузка пользователей...
';
+
+ try {
+ const response = await fetch('/api/webui/acl/users');
+ const data = await response.json();
+
+ if (data.success) {
+ contentArea.innerHTML = `
+
+
+
+
+
Пользователи
+
+
+ | Имя пользователя | Роли | Статус | Создан | Последний вход | Действия |
+
+
+ ${data.data.map(user => `
+
+ | ${escapeHtml(user.username)} |
+ ${user.roles.map(r => `${escapeHtml(r)}`).join(' ') || '-'} |
+ ${user.active ? 'Активен' : 'Отключён'} |
+ ${new Date(user.created_at).toLocaleString()} |
+ ${user.last_login ? new Date(user.last_login).toLocaleString() : '-'} |
+
+
+
+ |
+
+ `).join('')}
+
+
+
+ `;
+ } else {
+ contentArea.innerHTML = 'Ошибка загрузки пользователей
';
+ }
+ } catch (error) {
+ contentArea.innerHTML = 'Ошибка подключения
';
+ }
+}
+
+// Загрузка списка ролей
+async function loadACLRoles() {
+ pageTitle.textContent = 'Управление ролями ACL';
+ contentArea.innerHTML = '';
+
+ try {
+ const response = await fetch('/api/webui/acl/roles');
+ const data = await response.json();
+
+ if (data.success) {
+ contentArea.innerHTML = `
+
+
+
+
+
Роли
+
+
+ | Название роли | Разрешения | Действия |
+
+
+ ${data.data.map(role => `
+
+ | ${escapeHtml(role.name)} |
+ ${role.permissions.map(p => `${escapeHtml(p)}`).join(' ') || '-'} |
+
+
+
+ |
+
+ `).join('')}
+
+
+
+ `;
+ } else {
+ contentArea.innerHTML = 'Ошибка загрузки ролей
';
+ }
+ } catch (error) {
+ contentArea.innerHTML = 'Ошибка подключения
';
+ }
+}
+
+// Загрузка разрешений
+async function loadACLPermissions() {
+ pageTitle.textContent = 'Управление разрешениями ACL';
+ contentArea.innerHTML = '';
+
+ try {
+ const response = await fetch('/api/webui/acl/permissions');
+ const data = await response.json();
+
+ if (data.success) {
+ let html = 'Разрешения по ролям
';
+
+ for (const [roleName, permissions] of Object.entries(data.data)) {
+ html += `
+
+
Роль: ${escapeHtml(roleName)}
+
+ ${permissions.map(p => `${escapeHtml(p)}`).join('') || 'Нет разрешений'}
+
+
+ `;
+ }
+
+ html += '
';
+ contentArea.innerHTML = html;
+ } else {
+ contentArea.innerHTML = 'Ошибка загрузки разрешений
';
+ }
+ } catch (error) {
+ contentArea.innerHTML = 'Ошибка подключения
';
+ }
+}
+
+// Показать модальное окно создания пользователя
+function showCreateUserModal() {
+ modalTitle.textContent = 'Создать пользователя';
+ modalConfirm.textContent = 'Создать';
+ modalBody.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.classList.add('show');
+
+ modalConfirm.onclick = async () => {
+ const username = document.getElementById('username').value;
+ const password = document.getElementById('password').value;
+ const rolesStr = document.getElementById('roles').value;
+ const roles = rolesStr ? rolesStr.split(',').map(r => r.trim()) : [];
+
+ if (!username || !password) {
+ showNotification('Заполните имя пользователя и пароль', 'error');
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ password, roles })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ modal.classList.remove('show');
+ showNotification(`Пользователь ${username} создан`, 'success');
+ loadACLUsers();
+ } else {
+ showNotification(data.error || 'Ошибка создания пользователя', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения', 'error');
+ }
+ };
+}
+
+// Показать модальное окно редактирования пользователя
+function showEditUserModal(username, currentRoles) {
+ modalTitle.textContent = `Редактировать пользователя: ${username}`;
+ modalConfirm.textContent = 'Сохранить';
+ modalBody.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.classList.add('show');
+
+ modalConfirm.onclick = async () => {
+ const newPassword = document.getElementById('newPassword').value;
+ const addRole = document.getElementById('addRole').value;
+ const removeRole = document.getElementById('removeRole').value;
+
+ const updates = {};
+ if (newPassword) updates.password = newPassword;
+ if (addRole) updates.add_role = addRole;
+ if (removeRole) updates.remove_role = removeRole;
+
+ if (Object.keys(updates).length === 0) {
+ showNotification('Нет изменений для сохранения', 'warning');
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(updates)
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ modal.classList.remove('show');
+ showNotification(`Пользователь ${username} обновлён`, 'success');
+ loadACLUsers();
+ } else {
+ showNotification(data.error || 'Ошибка обновления', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения', 'error');
+ }
+ };
+}
+
+// Показать модальное окно создания роли
+function showCreateRoleModal() {
+ modalTitle.textContent = 'Создать роль';
+ modalConfirm.textContent = 'Создать';
+ modalBody.innerHTML = `
+
+
+
+
+ `;
+
+ modal.classList.add('show');
+
+ modalConfirm.onclick = async () => {
+ const roleName = document.getElementById('roleName').value;
+
+ if (!roleName) {
+ showNotification('Введите название роли', 'error');
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}`, {
+ method: 'POST'
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ modal.classList.remove('show');
+ showNotification(`Роль ${roleName} создана`, 'success');
+ loadACLRoles();
+ } else {
+ showNotification(data.error || 'Ошибка создания роли', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения', 'error');
+ }
+ };
+}
+
+// Показать модальное окно редактирования роли
+function showEditRoleModal(roleName, currentPermissions) {
+ modalTitle.textContent = `Редактировать роль: ${roleName}`;
+ modalConfirm.textContent = 'Добавить разрешение';
+ modalBody.innerHTML = `
+
+
+
+
+ Формат: database.collection:read|write|delete|admin (можно использовать * как wildcard)
+
+ `;
+
+ modal.classList.add('show');
+
+ modalConfirm.onclick = async () => {
+ const permission = document.getElementById('newPermission').value;
+
+ if (!permission) {
+ showNotification('Введите разрешение', 'error');
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}/grant/${encodeURIComponent(permission)}`, {
+ method: 'PUT'
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ showNotification(`Разрешение ${permission} добавлено`, 'success');
+ modal.classList.remove('show');
+ loadACLRoles();
+ } else {
+ showNotification(data.error || 'Ошибка добавления разрешения', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения', 'error');
+ }
+ };
+}
+
+// ==================== Transaction Functions ====================
+
+// Загрузка списка транзакций
+async function loadTransactionList() {
+ pageTitle.textContent = 'Активные транзакции';
+ contentArea.innerHTML = '';
+
+ try {
+ const response = await fetch('/api/webui/transactions');
+ const data = await response.json();
+
+ if (data.success) {
+ contentArea.innerHTML = `
+
+
+
+
+
+
+
+
Активные транзакции
+
+
+ | ID транзакции | Статус | Начало | Операций |
+
+
+ ${data.data.map(tx => `
+
+ ${escapeHtml(tx.id)} |
+ ${escapeHtml(tx.status)} |
+ ${new Date(tx.start_time).toLocaleString()} |
+ ${tx.operation_count || 0} |
+
+ `).join('') || '| Нет активных транзакций |
'}
+
+
+
+ `;
+ } else {
+ contentArea.innerHTML = 'Ошибка загрузки транзакций
';
+ }
+ } catch (error) {
+ contentArea.innerHTML = 'Ошибка подключения
';
+ }
+}
+
+// ==================== Index Functions ====================
+
+// Загрузка списка индексов
+async function loadIndexesList() {
+ pageTitle.textContent = 'Управление индексами';
+ contentArea.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
Выберите базу данных и коллекцию
+
+ `;
+
+ // Загружаем список БД
+ try {
+ const response = await fetch('/api/webui/databases');
+ const data = await response.json();
+
+ if (data.success) {
+ const dbSelect = document.getElementById('indexDbSelect');
+ dbSelect.innerHTML = '' +
+ data.data.map(db => ``).join('');
+ }
+ } catch (error) {
+ showNotification('Ошибка загрузки БД', 'error');
+ }
+}
+
+// Загрузка коллекций для выбранной БД
+window.loadCollectionsForIndex = async function() {
+ const dbName = document.getElementById('indexDbSelect').value;
+ const collSelect = document.getElementById('indexCollSelect');
+
+ if (!dbName) {
+ collSelect.innerHTML = '';
+ document.getElementById('indexesContent').innerHTML = 'Выберите базу данных и коллекцию
';
+ return;
+ }
+
+ collSelect.innerHTML = '';
+
+ try {
+ const response = await fetch(`/api/webui/collections/${encodeURIComponent(dbName)}`);
+ const data = await response.json();
+
+ if (data.success && data.data.collections) {
+ collSelect.innerHTML = '' +
+ data.data.collections.map(coll => ``).join('');
+ } else {
+ collSelect.innerHTML = '';
+ }
+ } catch (error) {
+ collSelect.innerHTML = '';
+ }
+};
+
+// Загрузка индексов для выбранной коллекции
+async function loadIndexesForCollection() {
+ const dbName = document.getElementById('indexDbSelect').value;
+ const collName = document.getElementById('indexCollSelect').value;
+
+ if (!dbName || !collName) {
+ document.getElementById('indexesContent').innerHTML = 'Выберите базу данных и коллекцию
';
+ return;
+ }
+
+ document.getElementById('indexesContent').innerHTML = '';
+
+ try {
+ const response = await fetch(`/api/webui/indexes/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}`);
+ const data = await response.json();
+
+ if (data.success) {
+ document.getElementById('indexesContent').innerHTML = `
+ Индексы коллекции ${escapeHtml(dbName)}.${escapeHtml(collName)}
+
+
+ | Имя индекса | Поля | Уникальный | Действия |
+
+
+ ${data.data.map(idx => `
+
+ ${escapeHtml(idx.name)} |
+ ${idx.fields.join(', ')} |
+ ${idx.unique ? 'Да' : 'Нет'} |
+
+
+ |
+
+ `).join('') || '| Нет индексов |
'}
+
+
+ `;
+ } else {
+ document.getElementById('indexesContent').innerHTML = 'Ошибка загрузки индексов
';
+ }
+ } catch (error) {
+ document.getElementById('indexesContent').innerHTML = 'Ошибка подключения
';
+ }
+}
+
+// ==================== Additional features for transactions ====================
+
+// Загрузка деталей транзакции
+async function loadTransactionDetails(txId) {
+ modalTitle.textContent = `Детали транзакции ${txId}`;
+ modalConfirm.textContent = 'Закрыть';
+ modalBody.innerHTML = '';
+
+ modal.classList.add('show');
+
+ try {
+ const response = await fetch(`/api/webui/transaction/${txId}/details`);
+ const data = await response.json();
+
+ if (data.success && data.data) {
+ const tx = data.data;
+ let operationsHtml = '';
+
+ if (tx.operations && tx.operations.length > 0) {
+ operationsHtml = `
+ Операции (${tx.operations.length})
+
+
+ | Тип | БД | Коллекция | ID документа |
+
+
+ ${tx.operations.map(op => `
+
+ | ${escapeHtml(op.type)} |
+ ${escapeHtml(op.database)} |
+ ${escapeHtml(op.collection)} |
+ ${escapeHtml(op.document_id)} |
+
+ `).join('')}
+
+
+ `;
+ } else {
+ operationsHtml = 'Нет операций в этой транзакции
';
+ }
+
+ modalBody.innerHTML = `
+
+
+ ID: ${escapeHtml(tx.id)}
+
+
+ Статус:
+ ${escapeHtml(tx.status)}
+
+
+ Время начала: ${new Date(tx.start_time).toLocaleString()}
+
+
+ Количество операций: ${tx.operation_count}
+
+
+ ${operationsHtml}
+
+ `;
+ } else {
+ modalBody.innerHTML = `Ошибка загрузки деталей: ${data.error || 'Неизвестная ошибка'}
`;
+ }
+ } catch (error) {
+ modalBody.innerHTML = 'Ошибка подключения
';
+ }
+
+ // Переопределяем обработчик кнопки подтверждения для закрытия
+ const originalConfirmHandler = modalConfirm.onclick;
+ modalConfirm.onclick = () => {
+ modal.classList.remove('show');
+ modalConfirm.onclick = originalConfirmHandler;
+ };
+}
+
+// Обновлённая функция loadTransactionList с детализацией
+const originalLoadTransactionList = window.loadTransactionList;
+window.loadTransactionList = async function() {
+ pageTitle.textContent = 'Активные транзакции';
+ contentArea.innerHTML = '';
+
+ try {
+ const response = await fetch('/api/webui/transactions');
+ const data = await response.json();
+
+ if (data.success) {
+ contentArea.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('') || '| Нет активных транзакций |
'}
+
+
+
+
+
+
+
Информация о транзакциях:
+
+ - Транзакции поддерживают операции INSERT, UPDATE, DELETE
+ - Используется MVCC (Multi-Version Concurrency Control) для изоляции
+ - WAL (Write-Ahead Logging) гарантирует持久化 операций
+ - При сбое системы незавершённые транзакции автоматически откатываются
+
+
+ `;
+ } else {
+ contentArea.innerHTML = 'Ошибка загрузки транзакций
';
+ }
+ } catch (error) {
+ contentArea.innerHTML = 'Ошибка подключения
';
+ }
+};
+
+// Функция обновления списка транзакций
+function refreshTransactionList() {
+ loadTransactionList();
+}
+
+// Функция коммита транзакции по ID
+async function commitTransactionById(txId) {
+ if (confirm(`Зафиксировать транзакцию ${txId}?`)) {
+ 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
+async function abortTransactionById(txId) {
+ if (confirm(`Отменить транзакцию ${txId}?`)) {
+ 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');
+ }
+ }
+}
+
+// ==================== Import/Export Functions ====================
+
+// Загрузка страницы экспорта
+async function loadExportPage() {
+ pageTitle.textContent = 'Экспорт данных';
+ contentArea.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ // Загружаем список БД
+ try {
+ const response = await fetch('/api/webui/databases');
+ const data = await response.json();
+
+ if (data.success) {
+ const dbSelect = document.getElementById('exportDbSelect');
+ dbSelect.innerHTML = '' +
+ data.data.map(db => ``).join('');
+ }
+ } catch (error) {
+ showNotification('Ошибка загрузки БД', 'error');
+ }
+}
+
+// Выполнение экспорта
+async function performExport() {
+ const dbName = document.getElementById('exportDbSelect').value;
+ const filename = document.getElementById('exportFilename').value;
+
+ if (!dbName) {
+ showNotification('Выберите базу данных', 'error');
+ return;
+ }
+
+ const exportResult = document.getElementById('exportResult');
+ exportResult.innerHTML = '';
+
+ try {
+ const response = await fetch('/api/webui/export', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ database: dbName, filename })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ // Создаём JSON файл для скачивания
+ const exportData = data.data.data;
+ const jsonStr = JSON.stringify(exportData, null, 2);
+ const blob = new Blob([jsonStr], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = data.data.filename.replace('.msgpack', '.json');
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ exportResult.innerHTML = `
+
+
+
Экспорт завершён
+
БД: ${escapeHtml(dbName)}
+
Коллекций: ${data.data.collections}
+
Файл: ${data.data.filename}
+
+ `;
+ showNotification('Экспорт завершён', 'success');
+ } else {
+ exportResult.innerHTML = `Ошибка: ${data.error}
`;
+ }
+ } catch (error) {
+ exportResult.innerHTML = 'Ошибка подключения
';
+ }
+}
+
+// Загрузка страницы импорта
+async function loadImportPage() {
+ pageTitle.textContent = 'Импорт данных';
+ contentArea.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+}
+
+// Выполнение импорта
+async function performImport() {
+ const dbName = document.getElementById('importDbName').value;
+ const fileInput = document.getElementById('importFile');
+ const overwrite = document.getElementById('importOverwrite').checked;
+
+ if (!dbName) {
+ showNotification('Введите имя целевой базы данных', 'error');
+ return;
+ }
+
+ if (!fileInput.files || fileInput.files.length === 0) {
+ showNotification('Выберите файл для импорта', 'error');
+ return;
+ }
+
+ const importResult = document.getElementById('importResult');
+ importResult.innerHTML = '';
+
+ try {
+ const file = fileInput.files[0];
+ const fileContent = await file.text();
+ const importData = JSON.parse(fileContent);
+
+ const response = await fetch('/api/webui/import', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ database: dbName, data: importData, overwrite })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ importResult.innerHTML = `
+
+
+
Импорт завершён
+
БД: ${escapeHtml(dbName)}
+
Импортировано коллекций: ${data.data.collections}
+
Импортировано документов: ${data.data.documents}
+
+ `;
+ showNotification('Импорт завершён', 'success');
+ } else {
+ importResult.innerHTML = `Ошибка: ${data.error}
`;
+ }
+ } catch (error) {
+ importResult.innerHTML = `Ошибка: ${error.message}
`;
+ }
+}
+
+// ==================== CRUD Action Handlers ====================
+
+// Обработка CRUD действий
+function handleCrudAction(action) {
+ switch(action) {
+ case 'create-db':
+ showCreateDatabaseModal();
+ break;
+ case 'create-collection':
+ showCreateCollectionModal();
+ break;
+ case 'insert-doc':
+ showInsertDocumentModal();
+ break;
+ case 'find-doc':
+ showFindDocumentModal();
+ break;
+ case 'update-doc':
+ showUpdateDocumentModal();
+ break;
+ case 'delete-doc':
+ showDeleteDocumentModal();
+ break;
+ case 'acl-create-user':
+ showCreateUserModal();
+ break;
+ case 'acl-create-role':
+ showCreateRoleModal();
+ break;
+ case 'tx-start-session':
+ startSession();
+ break;
+ case 'tx-start':
+ startTransaction();
+ break;
+ case 'tx-commit':
+ commitTransaction();
+ break;
+ case 'tx-abort':
+ abortTransaction();
+ break;
+ case 'index-create':
+ showCreateIndexModal();
+ break;
+ }
+}
+
+// Показать модальное окно создания БД
+function showCreateDatabaseModal() {
+ modalTitle.textContent = 'Создать базу данных';
+ modalConfirm.textContent = 'Подтвердить';
+ modalBody.innerHTML = `
+
+
+
+
+ `;
+
+ modal.classList.add('show');
+
+ modalConfirm.onclick = async () => {
+ const dbName = document.getElementById('dbName').value;
+ if (!dbName) {
+ showNotification('Введите имя базы данных', 'error');
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/db/' + dbName, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({})
+ });
+
+ if (response.ok) {
+ 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;
+ }
+
+ modalTitle.textContent = 'Создать коллекцию';
+ modalConfirm.textContent = 'Подтвердить';
+ modalBody.innerHTML = `
+
+
+
+
+
+
+
+
+ `;
+
+ modal.classList.add('show');
+
+ 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',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({})
+ });
+
+ if (response.ok) {
+ 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;
+ }
+
+ modalTitle.textContent = 'Вставить документ';
+ modalConfirm.textContent = 'Подтвердить';
+ modalBody.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.classList.add('show');
+
+ 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) {
+ modal.classList.remove('show');
+ showNotification('Документ вставлен', 'success');
+ viewCollection(currentDatabase, currentCollection);
+ } else {
+ showNotification(result.error || 'Ошибка вставки документа', 'error');
+ }
+ } catch (error) {
+ if (error instanceof SyntaxError) {
+ showNotification('Неверный формат JSON', 'error');
+ } else {
+ showNotification('Ошибка подключения', 'error');
+ }
+ }
+ };
+}
+
+// Показать модальное окно поиска документа
+function showFindDocumentModal() {
+ if (!currentDatabase || !currentCollection) {
+ showNotification('Сначала выберите базу данных и коллекцию', 'warning');
+ return;
+ }
+
+ modalTitle.textContent = 'Найти документ';
+ modalConfirm.textContent = 'Найти';
+ modalBody.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.classList.add('show');
+
+ modalConfirm.onclick = async () => {
+ const docId = document.getElementById('docId').value;
+ if (!docId) {
+ showNotification('Введите ID документа', 'error');
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/db/${currentDatabase}/${currentCollection}/${docId}`);
+
+ if (response.ok) {
+ const data = await response.json();
+ modal.classList.remove('show');
+
+ contentArea.innerHTML = `
+
+
Результат поиска
+
+ ${escapeHtml(JSON.stringify(data.data, null, 2))}
+
+
+
+ `;
+ } else {
+ const error = await response.json();
+ showNotification(error.error || 'Документ не найден', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения', 'error');
+ }
+ };
+}
+
+// Показать модальное окно обновления документа
+function showUpdateDocumentModal(docId, currentFields = null) {
+ if (!currentDatabase || !currentCollection) {
+ showNotification('Сначала выберите базу данных и коллекцию', 'warning');
+ return;
+ }
+
+ const fieldsJson = currentFields ? JSON.stringify(currentFields, null, 2) : '';
+
+ modalTitle.textContent = 'Обновить документ';
+ modalConfirm.textContent = 'Обновить';
+ modalBody.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.classList.add('show');
+
+ 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) {
+ modal.classList.remove('show');
+ showNotification('Документ обновлён', 'success');
+ viewCollection(currentDatabase, currentCollection);
+ } else {
+ showNotification(result.error || 'Ошибка обновления документа', 'error');
+ }
+ } catch (error) {
+ if (error instanceof SyntaxError) {
+ showNotification('Неверный формат JSON', 'error');
+ } else {
+ showNotification('Ошибка подключения', 'error');
+ }
+ }
+ };
+}
+
+// Показать модальное окно удаления документа
+function showDeleteDocumentModal() {
+ if (!currentDatabase || !currentCollection) {
+ showNotification('Сначала выберите базу данных и коллекцию', 'warning');
+ return;
+ }
+
+ modalTitle.textContent = 'Удалить документ';
+ modalConfirm.textContent = 'Удалить';
+ modalBody.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.classList.add('show');
+
+ modalConfirm.onclick = async () => {
+ const docId = document.getElementById('deleteDocId').value;
+ if (!docId) {
+ showNotification('Введите ID документа', 'error');
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}?id=${encodeURIComponent(docId)}`, {
+ method: 'DELETE'
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ modal.classList.remove('show');
+ showNotification('Документ удалён', 'success');
+ viewCollection(currentDatabase, currentCollection);
+ } else {
+ showNotification(result.error || 'Ошибка удаления документа', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения', 'error');
+ }
+ };
+}
+
+// Показать модальное окно создания индекса
+function showCreateIndexModal() {
+ const dbName = document.getElementById('indexDbSelect')?.value;
+ const collName = document.getElementById('indexCollSelect')?.value;
+
+ if (!dbName || !collName) {
+ showNotification('Сначала выберите базу данных и коллекцию на странице "Список индексов"', 'warning');
+ return;
+ }
+
+ modalTitle.textContent = 'Создать индекс';
+ modalConfirm.textContent = 'Создать';
+ modalBody.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.classList.add('show');
+
+ modalConfirm.onclick = async () => {
+ const indexName = document.getElementById('indexName').value;
+ const fieldsStr = document.getElementById('indexFields').value;
+ const unique = document.getElementById('indexUnique').checked;
+
+ if (!indexName || !fieldsStr) {
+ showNotification('Заполните имя индекса и поля', 'error');
+ return;
+ }
+
+ const fields = fieldsStr.split(',').map(f => f.trim());
+
+ try {
+ const response = await fetch(`/api/webui/index/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/create`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name: indexName, fields, unique })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ modal.classList.remove('show');
+ showNotification(`Индекс ${indexName} создан`, 'success');
+ loadIndexesForCollection();
+ } else {
+ showNotification(data.error || 'Ошибка создания индекса', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения', 'error');
+ }
+ };
+}
+
+// ==================== Вспомогательные функции ====================
+
+// Удаление коллекции
+window.deleteCollection = async function(dbName, collName) {
+ if (confirm(`Вы уверены, что хотите удалить коллекцию "${collName}"? Это действие необратимо.`)) {
+ 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');
+ }
+ }
+};
+
+// Удаление документа
+window.deleteDocument = async function(dbName, collName, docId) {
+ if (confirm(`Вы уверены, что хотите удалить документ "${docId}"?`)) {
+ 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');
+ }
+ }
+};
+
+// Удаление пользователя
+window.deleteUser = async function(username) {
+ if (confirm(`Удалить пользователя "${username}"?`)) {
+ try {
+ const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, {
+ method: 'DELETE'
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ showNotification(`Пользователь ${username} удалён`, 'success');
+ loadACLUsers();
+ } else {
+ showNotification(data.error || 'Ошибка удаления', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения', 'error');
+ }
+ }
+};
+
+// Удаление роли
+window.deleteRole = async function(roleName) {
+ if (confirm(`Удалить роль "${roleName}"?`)) {
+ try {
+ const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}`, {
+ method: 'DELETE'
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ showNotification(`Роль ${roleName} удалена`, 'success');
+ loadACLRoles();
+ } else {
+ showNotification(data.error || 'Ошибка удаления', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения', 'error');
+ }
+ }
+};
+
+// Отключение пользователя
+window.disableUser = async function(username) {
+ try {
+ const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ disable: true })
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ showNotification(`Пользователь ${username} отключён`, 'success');
+ loadACLUsers();
+ } else {
+ showNotification(data.error || 'Ошибка', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения', 'error');
+ }
+};
+
+// Включение пользователя
+window.enableUser = async function(username) {
+ try {
+ const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ enable: true })
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ showNotification(`Пользователь ${username} включён`, 'success');
+ loadACLUsers();
+ } else {
+ showNotification(data.error || 'Ошибка', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения', 'error');
+ }
+};
+
+// Отзыв разрешения у роли
+window.revokePermission = async function(roleName, permission) {
+ try {
+ const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}/revoke/${encodeURIComponent(permission)}`, {
+ method: 'PUT'
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ showNotification(`Разрешение отозвано`, 'success');
+ loadACLRoles();
+ } else {
+ showNotification(data.error || 'Ошибка', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения', 'error');
+ }
+};
+
+// Удаление индекса
+window.dropIndex = async function(dbName, collName, indexName) {
+ if (confirm(`Удалить индекс "${indexName}"?`)) {
+ try {
+ const response = await fetch(`/api/webui/index/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/drop/${encodeURIComponent(indexName)}`, {
+ method: 'POST'
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ showNotification(`Индекс ${indexName} удалён`, 'success');
+ loadIndexesForCollection();
+ } else {
+ showNotification(data.error || 'Ошибка удаления индекса', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения', 'error');
+ }
+ }
+};
+
+// Функции для транзакций
+window.startSession = async function() {
+ 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');
+ }
+};
+
+window.startTransaction = async function() {
+ 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');
+ }
+};
+
+window.commitTransaction = async function() {
+ 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');
+ }
+};
+
+window.abortTransaction = async function() {
+ 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');
+ }
+};
+
+// Сохранение настроек
+function saveSettings() {
+ const theme = document.getElementById('themeSelect')?.value;
+ if (theme) {
+ localStorage.setItem('theme', theme);
+ showNotification('Настройки сохранены', 'success');
+ }
+}
+
+// Утилиты
+function escapeHtml(str) {
+ if (!str) return '';
+ return String(str)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
+
+function showNotification(message, type = 'info') {
+ const container = document.getElementById('notificationContainer');
+ const notification = document.createElement('div');
+ notification.className = `notification ${type}`;
+
+ let icon = '';
+ switch(type) {
+ case 'success': icon = ''; break;
+ case 'error': icon = ''; break;
+ case 'warning': icon = ''; break;
+ default: icon = '';
+ }
+
+ notification.innerHTML = `${icon}${escapeHtml(message)}`;
+ container.appendChild(notification);
+
+ setTimeout(() => {
+ notification.style.animation = 'slideOutRight 0.3s ease';
+ setTimeout(() => notification.remove(), 300);
+ }, 3000);
+}
+
+function setActiveNav(activeLink) {
+ document.querySelectorAll('.nav-link').forEach(link => {
+ link.classList.remove('active');
+ });
+ activeLink.classList.add('active');
+}
+
+// ==================== Trigger Functions ====================
+
+// Загрузка списка триггеров
+async function loadTriggersList() {
+ pageTitle.textContent = 'Управление триггерами';
+ contentArea.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
Выберите базу данных и коллекцию
+
+ `;
+
+ // Загружаем список БД
+ try {
+ const response = await fetch('/api/webui/databases');
+ const data = await response.json();
+
+ if (data.success) {
+ const dbSelect = document.getElementById('triggerDbSelect');
+ dbSelect.innerHTML = '' +
+ data.data.map(db => ``).join('');
+ }
+ } catch (error) {
+ showNotification('Ошибка загрузки БД', 'error');
+ }
+}
+
+// Загрузка коллекций для выбранной БД (для триггеров)
+window.loadCollectionsForTrigger = async function() {
+ const dbName = document.getElementById('triggerDbSelect').value;
+ const collSelect = document.getElementById('triggerCollSelect');
+
+ if (!dbName) {
+ collSelect.innerHTML = '';
+ document.getElementById('triggersContent').innerHTML = 'Выберите базу данных и коллекцию
';
+ return;
+ }
+
+ collSelect.innerHTML = '';
+
+ try {
+ const response = await fetch(`/api/webui/collections/${encodeURIComponent(dbName)}`);
+ const data = await response.json();
+
+ if (data.success && data.data.collections) {
+ collSelect.innerHTML = '' +
+ data.data.collections.map(coll => ``).join('');
+ } else {
+ collSelect.innerHTML = '';
+ }
+ } catch (error) {
+ collSelect.innerHTML = '';
+ }
+};
+
+// Загрузка триггеров для выбранной коллекции
+async function loadTriggersForCollection() {
+ const dbName = document.getElementById('triggerDbSelect').value;
+ const collName = document.getElementById('triggerCollSelect').value;
+
+ if (!dbName || !collName) {
+ document.getElementById('triggersContent').innerHTML = 'Выберите базу данных и коллекцию
';
+ return;
+ }
+
+ document.getElementById('triggersContent').innerHTML = '';
+
+ try {
+ const response = await fetch(`/api/webui/triggers/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}`);
+ const data = await response.json();
+
+ if (data.success) {
+ if (data.data.length === 0) {
+ document.getElementById('triggersContent').innerHTML = 'Нет триггеров для этой коллекции
';
+ return;
+ }
+
+ document.getElementById('triggersContent').innerHTML = `
+ Триггеры коллекции ${escapeHtml(dbName)}.${escapeHtml(collName)}
+
+
+ | Имя | Событие | Действие | Статус | Описание | Действия |
+
+
+ ${data.data.map(trigger => `
+
+ ${escapeHtml(trigger.name)} |
+ ${escapeHtml(trigger.event)} |
+ ${escapeHtml(trigger.action)} |
+ ${trigger.enabled ? 'Включён' : 'Отключён'} |
+ ${escapeHtml(trigger.description || '-')} |
+
+ ${trigger.enabled ?
+ `` :
+ ``
+ }
+
+ |
+
+ `).join('')}
+
+
+ `;
+ } else {
+ document.getElementById('triggersContent').innerHTML = 'Ошибка загрузки триггеров
';
+ }
+ } catch (error) {
+ document.getElementById('triggersContent').innerHTML = 'Ошибка подключения
';
+ }
+}
+
+// Показать модальное окно создания триггера
+function showCreateTriggerModal() {
+ const dbName = document.getElementById('triggerDbSelect').value;
+ const collName = document.getElementById('triggerCollSelect').value;
+
+ if (!dbName || !collName) {
+ showNotification('Сначала выберите базу данных и коллекцию на странице "Список триггеров"', 'warning');
+ return;
+ }
+
+ modalTitle.textContent = 'Создать триггер';
+ modalConfirm.textContent = 'Создать';
+ modalBody.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Доступные операции: set, unset, inc, mul, rename, currentDate
+ Специальные значения: $$NOW (текущее время), $$USER (текущий пользователь), $$ROLE (текущая роль)
+
+
+ `;
+
+ modal.classList.add('show');
+
+ modalConfirm.onclick = async () => {
+ const triggerName = document.getElementById('triggerName').value;
+ const triggerEvent = document.getElementById('triggerEvent').value;
+ const triggerAction = document.getElementById('triggerAction').value;
+ const triggerDescription = document.getElementById('triggerDescription').value;
+
+ if (!triggerName) {
+ showNotification('Введите имя триггера', 'error');
+ return;
+ }
+
+ // Собираем условие
+ let condition = null;
+ const conditionField = document.getElementById('conditionField').value;
+ if (conditionField) {
+ condition = {
+ field: conditionField,
+ operator: document.getElementById('conditionOperator').value,
+ value: document.getElementById('conditionValue').value
+ };
+ // Преобразуем числовые значения
+ if (condition.value && !isNaN(condition.value) && condition.value.trim() !== '') {
+ condition.value = parseFloat(condition.value);
+ }
+ }
+
+ // Парсим операции
+ let operations = [];
+ const opsText = document.getElementById('triggerOperations').value;
+ if (opsText && opsText.trim()) {
+ try {
+ operations = JSON.parse(opsText);
+ } catch (e) {
+ showNotification('Неверный формат JSON для операций', 'error');
+ return;
+ }
+ }
+
+ const requestBody = {
+ name: triggerName,
+ event: triggerEvent,
+ action: triggerAction,
+ description: triggerDescription,
+ operations: operations
+ };
+
+ if (condition) {
+ requestBody.condition = condition;
+ }
+
+ try {
+ const response = await fetch(`/api/webui/trigger/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/create`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(requestBody)
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ modal.classList.remove('show');
+ showNotification(`Триггер ${triggerName} создан`, 'success');
+ loadTriggersForCollection();
+ } else {
+ showNotification(data.error || 'Ошибка создания триггера', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения', 'error');
+ }
+ };
+}
+
+// Включение/отключение триггера
+window.toggleTrigger = async function(dbName, collName, triggerName, triggerEvent, enable) {
+ const action = enable ? 'enable' : 'disable';
+
+ try {
+ const response = await fetch(`/api/webui/trigger/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/${action}/${encodeURIComponent(triggerName)}`, {
+ method: 'POST'
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ showNotification(`Триггер ${triggerName} ${enable ? 'включён' : 'отключён'}`, 'success');
+ loadTriggersForCollection();
+ } else {
+ showNotification(data.error || 'Ошибка', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения', 'error');
+ }
+};
+
+// Удаление триггера
+window.deleteTrigger = async function(dbName, collName, triggerName, triggerEvent) {
+ if (confirm(`Удалить триггер "${triggerName}"?`)) {
+ try {
+ const response = await fetch(`/api/webui/trigger/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/delete/${encodeURIComponent(triggerName)}/${encodeURIComponent(triggerEvent)}`, {
+ method: 'DELETE'
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ showNotification(`Триггер ${triggerName} удалён`, 'success');
+ loadTriggersForCollection();
+ } else {
+ showNotification(data.error || 'Ошибка удаления', 'error');
+ }
+ } catch (error) {
+ showNotification('Ошибка подключения', 'error');
+ }
+ }
+};
+
+// Загрузка лога выполнения триггеров
+async function loadTriggerLog() {
+ pageTitle.textContent = 'Лог выполнения триггеров';
+ contentArea.innerHTML = '';
+
+ try {
+ const response = await fetch('/api/webui/trigger/log');
+ const data = await response.json();
+
+ if (data.success) {
+ if (data.data.length === 0) {
+ contentArea.innerHTML = 'Лог выполнения триггеров пуст
';
+ return;
+ }
+
+ contentArea.innerHTML = `
+
+
Лог выполнения триггеров
+
+
+ | Триггер | Событие | Коллекция | БД | Документ | Время | Пользователь |
+
+
+ ${data.data.map(entry => `
+
+ ${escapeHtml(entry.trigger_name)} |
+ ${escapeHtml(entry.event)} |
+ ${escapeHtml(entry.collection)} |
+ ${escapeHtml(entry.database)} |
+ ${escapeHtml(entry.document_id)} |
+ ${new Date(entry.timestamp).toLocaleString()} |
+ ${escapeHtml(entry.user || '-')} |
+
+ `).join('')}
+
+
+
+ `;
+ } else {
+ contentArea.innerHTML = 'Ошибка загрузки лога
';
+ }
+ } catch (error) {
+ contentArea.innerHTML = 'Ошибка подключения
';
+ }
+}
+
+// Обновляем loadSection для секции триггеров
+const originalLoadSectionTriggers = window.loadSection;
+window.loadSection = function(section) {
+ switch(section) {
+ case 'triggers-list':
+ loadTriggersList();
+ break;
+ case 'trigger-log':
+ loadTriggerLog();
+ break;
+ default:
+ if (originalLoadSectionTriggers) originalLoadSectionTriggers(section);
+ }
+};