/* * 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 => ` `).join('')}
Имя БДКоллекцииДействия
${escapeHtml(db.name)} ${db.collections}
`; } 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 => ` `).join('')}
Имя коллекцииДокументовРазмерИндексыДействия
${escapeHtml(coll.name)} ${coll.count} ${(coll.size / 1024).toFixed(2)} KB ${coll.indexes.length}
`; } 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} всего)

${data.data.documents.map(doc => ` `).join('')}
IDПоляСозданДействия
${escapeHtml(doc.id)}
${escapeHtml(JSON.stringify(doc.fields, null, 2))}
${new Date(doc.created_at).toLocaleString()}
`; } 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}

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

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

${nodes.data.map(node => ` `).join('')}
ID узлаАдресСтатусПоследний контакт
${escapeHtml(node.id)} ${escapeHtml(node.ip)}:${node.port} ${node.status} ${new Date(node.last_seen * 1000).toLocaleString()}
`; } 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 => ` `).join('')}
Имя пользователяРолиСтатусСозданПоследний входДействия
${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() : '-'}
`; } 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 => ` `).join('')}
Название ролиРазрешенияДействия
${escapeHtml(role.name)} ${role.permissions.map(p => `${escapeHtml(p)}`).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 = `
${currentPermissions.map(p => `
${escapeHtml(p)}
`).join('') || 'Нет разрешений'}
Формат: 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 = `

Активные транзакции

${data.data.map(tx => ` `).join('') || ''}
ID транзакцииСтатусНачалоОпераций
${escapeHtml(tx.id)} ${escapeHtml(tx.status)} ${new Date(tx.start_time).toLocaleString()} ${tx.operation_count || 0}
Нет активных транзакций
`; } 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 => ` `).join('') || ''}
Имя индексаПоляУникальныйДействия
${escapeHtml(idx.name)} ${idx.fields.join(', ')} ${idx.unique ? 'Да' : 'Нет'}
Нет индексов
`; } else { document.getElementById('indexesContent').innerHTML = '
Ошибка загрузки индексов
'; } } catch (error) { document.getElementById('indexesContent').innerHTML = '
Ошибка подключения
'; } } // ==================== 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 => ` `).join('')}
ИмяСобытиеДействиеСтатусОписаниеДействия
${escapeHtml(trigger.name)} ${escapeHtml(trigger.event)} ${escapeHtml(trigger.action)} ${trigger.enabled ? 'Включён' : 'Отключён'} ${escapeHtml(trigger.description || '-')} ${trigger.enabled ? `` : `` }
`; } 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 => ` `).join('')}
ТриггерСобытиеКоллекцияБДДокументВремяПользователь
${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 || '-')}
`; } 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); } };