diff --git a/internal/api/static/app.js b/internal/api/static/app.js
deleted file mode 100644
index 2defe18..0000000
--- a/internal/api/static/app.js
+++ /dev/null
@@ -1,1693 +0,0 @@
-/*
- * Copyright 2026 Safronov Grigorii
- *
- * Licensed under the CDDL, Version 1.0 (the "License");
- * you may not use this file except in compliance with the License.
- *
- * You may obtain a copy of the License at
- * https://opensource.org/licenses/CDDL-1.0
- */
-
-/**
- * @fileoverview JavaScript для веб-интерфейса Futriis DB Dashboard
- * @version 1.0.0
- * @description Обеспечивает полное управление СУБД: CRUD операции, ACL, индексы,
- * транзакции, триггеры, ограничения (constraints), импорт/экспорт,
- * управление кластером и аудит. Использует async/await, Fetch API,
- * динамическую отрисовку DOM и модальные окна.
- */
-
-// ============================== ГЛОБАЛЬНОЕ СОСТОЯНИЕ ==============================
-
-/** @type {string|null} ID текущей сессии */
-let currentSession = null;
-/** @type {string|null} Имя текущей базы данных */
-let currentDatabase = null;
-/** @type {string|null} Имя текущей коллекции */
-let currentCollection = null;
-/** @type {string|null} Имя текущего пользователя */
-let currentUser = null;
-
-// ============================== DOM ЭЛЕМЕНТЫ ==============================
-
-/**
- * @description 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');
-
-// ============================== ИНИЦИАЛИЗАЦИЯ ==============================
-
-/**
- * @description Главная точка входа. Выполняется после загрузки DOM.
- * Проверяет активную сессию, инициализирует навигацию и обработчики.
- */
-document.addEventListener('DOMContentLoaded', () => {
- checkSession();
- initNavigation();
- initEventListeners();
-});
-
-// ============================== АУТЕНТИФИКАЦИЯ И СЕССИЯ ==============================
-
-/**
- * @async
- * @description Проверяет активность сессии на сервере.
- * При успешной аутентификации загружает дашборд, иначе показывает форму входа.
- */
-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();
- startConnectionStatusMonitor();
- } else {
- showLoginModal();
- }
- } catch (error) {
- console.error('Session check failed:', error);
- showLoginModal();
- }
-}
-
-/**
- * @description Запускает периодическую проверку статуса подключения к СУБД.
- * Обновляет индикатор каждые 5 секунд.
- */
-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);
-}
-
-/**
- * @description Отображает модальное окно для входа в систему.
- * Обрабатывает отправку учётных данных и сохраняет сессию.
- */
-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);
-}
-
-// ============================== НАВИГАЦИЯ ==============================
-
-/**
- * @description Инициализирует обработчики навигации:
- * - Переключение секций (data-section)
- * - Выполнение действий (data-action)
- * - Раскрытие подменю (has-submenu)
- */
-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');
- });
- });
-}
-
-/**
- * @description Инициализирует глобальные обработчики событий:
- * - Выход из системы
- * - Мобильное меню
- * - Закрытие модальных окон
- */
-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
- * @description Загружает соответствующую секцию интерфейса.
- * @param {string} section - Идентификатор секции (значение data-section)
- */
-async function loadSection(section) {
- const sections = {
- dashboard: loadDashboard,
- cluster: loadClusterManagement,
- audit: () => { contentArea.innerHTML = 'Функция в разработке
'; },
- settings: loadSettings,
- 'acl-users': loadACLUsers,
- 'acl-roles': loadACLRoles,
- 'acl-permissions': loadACLPermissions,
- 'tx-list': loadTransactionList,
- 'indexes-list': loadIndexesList,
- 'export-data': loadExportPage,
- 'import-data': loadImportPage,
- 'constraints-list': loadConstraintsList,
- 'triggers-list': loadTriggersList,
- 'trigger-log': loadTriggerLog
- };
- (sections[section] || loadDashboard)();
-}
-
-// ============================== ДАШБОРД ==============================
-
-/**
- * @async
- * @description Загружает и отображает главную панель управления со статистикой и списком БД.
- */
-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');
- }
-}
-
-// ============================== БАЗЫ ДАННЫХ И КОЛЛЕКЦИИ ==============================
-
-/**
- * @async
- * @description Отображает список коллекций в выбранной базе данных.
- * @param {string} dbName - Имя базы данных
- */
-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 = 'Ошибка подключения
';
- }
-};
-
-/**
- * @async
- * @description Отображает документы выбранной коллекции с пагинацией и действиями.
- * @param {string} dbName - Имя базы данных
- * @param {string} collName - Имя коллекции
- */
-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
- * @description Загружает и отображает статус кластера и список узлов.
- */
-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 = 'Ошибка загрузки информации о кластере
';
- }
-}
-
-// ============================== НАСТРОЙКИ ==============================
-
-/**
- * @description Отображает страницу настроек интерфейса.
- */
-function loadSettings() {
- pageTitle.textContent = 'Настройки';
- contentArea.innerHTML = `
- Настройки интерфейса
-
-
- `;
-}
-
-// ============================== ACL (УПРАВЛЕНИЕ ДОСТУПОМ) ==============================
-
-/**
- * @async
- * @description Загружает и отображает список пользователей системы.
- */
-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
- * @description Загружает и отображает список ролей и их разрешений.
- */
-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
- * @description Загружает и отображает все разрешения, сгруппированные по ролям.
- */
-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 = 'Ошибка подключения
';
- }
-}
-
-// ============================== ТРАНЗАКЦИИ ==============================
-
-/**
- * @async
- * @description Загружает и отображает список активных транзакций с возможностью управления.
- */
-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} |
-
-
- ${tx.status === 'active' ? `` : ''}
- |
-
- `).join('') || '| Нет активных транзакций |
'}
-
- Информация о транзакциях:- Транзакции поддерживают INSERT, UPDATE, DELETE
- Используется MVCC для изоляции
- WAL гарантирует персистентность
- `;
- } else {
- contentArea.innerHTML = 'Ошибка загрузки транзакций
';
- }
- } catch (error) {
- contentArea.innerHTML = 'Ошибка подключения
';
- }
-}
-
-// ============================== ИНДЕКСЫ ==============================
-
-/**
- * @async
- * @description Загружает страницу управления индексами с выбором БД и коллекции.
- */
-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');
- }
-}
-
-/**
- * @description Загружает коллекции для выбранной БД в интерфейсе индексов.
- */
-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
- * @description Загружает и отображает список индексов выбранной коллекции.
- */
-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 = 'Ошибка подключения
';
- }
-}
-
-// ============================== ТРИГГЕРЫ ==============================
-
-/**
- * @async
- * @description Загружает страницу управления триггерами.
- */
-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');
- }
-}
-
-/**
- * @description Загружает коллекции для выбранной БД в интерфейсе триггеров.
- */
-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
- * @description Загружает и отображает список триггеров выбранной коллекции.
- */
-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 = 'Ошибка подключения
';
- }
-}
-
-/**
- * @async
- * @description Загружает и отображает лог выполнения триггеров.
- */
-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 = 'Ошибка подключения
';
- }
-}
-
-// ============================== ОГРАНИЧЕНИЯ (CONSTRAINTS) ==============================
-
-/**
- * @async
- * @description Загружает страницу управления ограничениями коллекции.
- */
-async function loadConstraintsList() {
- pageTitle.textContent = 'Управление ограничениями (Constraints)';
- contentArea.innerHTML = `
-
-
- Выберите базу данных и коллекцию
- `;
-
- try {
- const response = await fetch('/api/webui/databases');
- const data = await response.json();
- if (data.success) {
- const dbSelect = document.getElementById('constraintDbSelect');
- dbSelect.innerHTML = '' + data.data.map(db => ``).join('');
- }
- } catch (error) {
- showNotification('Ошибка загрузки БД', 'error');
- }
-}
-
-/**
- * @description Загружает коллекции для выбранной БД в интерфейсе ограничений.
- */
-window.loadCollectionsForConstraints = async function() {
- const dbName = document.getElementById('constraintDbSelect').value;
- const collSelect = document.getElementById('constraintCollSelect');
- if (!dbName) {
- collSelect.innerHTML = '';
- document.getElementById('constraintsContent').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
- * @description Загружает и отображает все ограничения выбранной коллекции.
- */
-async function loadConstraintsForCollection() {
- const dbName = document.getElementById('constraintDbSelect').value;
- const collName = document.getElementById('constraintCollSelect').value;
- if (!dbName || !collName) {
- document.getElementById('constraintsContent').innerHTML = 'Выберите базу данных и коллекцию
';
- return;
- }
-
- document.getElementById('constraintsContent').innerHTML = '';
- try {
- const response = await fetch(`/api/webui/constraints/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}`);
- const data = await response.json();
-
- if (data.success) {
- const constraints = data.data;
- if (constraints.length === 0) {
- document.getElementById('constraintsContent').innerHTML = 'Нет ограничений для этой коллекции
';
- return;
- }
-
- // Группировка ограничений по типу
- const grouped = { required: [], unique: [], min: [], max: [], enum: [], regex: [] };
- constraints.forEach(c => { if (grouped[c.type]) grouped[c.type].push(c); });
-
- let html = `Ограничения коллекции ${escapeHtml(dbName)}.${escapeHtml(collName)}
`;
- for (const [type, title, icon] of [['required', 'Обязательные поля', 'fa-exclamation-circle'], ['unique', 'Уникальные поля', 'fa-unique'], ['min', 'Минимальные значения', 'fa-greater-than'], ['max', 'Максимальные значения', 'fa-less-than'], ['enum', 'Перечисления', 'fa-list-ul'], ['regex', 'Регулярные выражения', 'fa-code']]) {
- if (grouped[type].length > 0) {
- html += `
${title}
| Поле | ${type === 'enum' ? 'Допустимые значения' : (type === 'min' || type === 'max' ? 'Значение' : (type === 'regex' ? 'Шаблон' : ''))} | Действия |
`;
- for (const c of grouped[type]) {
- html += `${escapeHtml(c.field)} | ${type === 'enum' ? c.values.map(v => `${escapeHtml(String(v))}`).join(' ') : (c.value || c.pattern || '-')} | |
`;
- }
- html += `
`;
- }
- }
- html += `
`;
- document.getElementById('constraintsContent').innerHTML = html;
- } else {
- document.getElementById('constraintsContent').innerHTML = 'Ошибка загрузки ограничений
';
- }
- } catch (error) {
- document.getElementById('constraintsContent').innerHTML = 'Ошибка подключения
';
- }
-}
-
-// ============================== ИМПОРТ/ЭКСПОРТ ==============================
-
-/**
- * @async
- * @description Загружает страницу экспорта данных.
- */
-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
- * @description Выполняет экспорт данных в JSON-файл.
- */
-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) {
- const jsonStr = JSON.stringify(data.data.data, 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
- * @description Загружает страницу импорта данных.
- */
-async function loadImportPage() {
- pageTitle.textContent = 'Импорт данных';
- contentArea.innerHTML = `
-
-
-
-
-
- `;
-}
-
-/**
- * @async
- * @description Выполняет импорт данных из JSON-файла.
- */
-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 ОПЕРАЦИИ (МОДАЛЬНЫЕ ОКНА) ==============================
-
-/**
- * @description Отображает модальное окно для создания базы данных.
- */
-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'); }
- };
-}
-
-/**
- * @description Отображает модальное окно для создания коллекции в текущей БД.
- */
-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'); }
- };
-}
-
-/**
- * @description Отображает модальное окно для вставки документа в текущую коллекцию.
- */
-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) {
- showNotification(error instanceof SyntaxError ? 'Неверный формат JSON' : 'Ошибка подключения', 'error');
- }
- };
-}
-
-/**
- * @description Отображает модальное окно для обновления документа.
- * @param {string} docId - ID документа (опционально)
- * @param {Object|null} currentFields - Текущие поля документа
- */
-function showUpdateDocumentModal(docId = '', currentFields = null) {
- if (!currentDatabase || !currentCollection) { showNotification('Сначала выберите базу данных и коллекцию', 'warning'); return; }
- 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) {
- showNotification(error instanceof SyntaxError ? 'Неверный формат JSON' : 'Ошибка подключения', 'error');
- }
- };
-}
-
-/**
- * @description Отображает модальное окно для создания пользователя ACL.
- */
-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'); }
- };
-}
-
-/**
- * @description Отображает модальное окно для создания роли ACL.
- */
-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'); }
- };
-}
-
-/**
- * @description Отображает модальное окно для создания индекса.
- */
-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'); }
- };
-}
-
-/**
- * @description Отображает модальное окно для создания триггера.
- */
-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 };
- 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'); }
- };
-}
-
-// ============================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ==============================
-
-/**
- * @description Устанавливает активный пункт навигации.
- * @param {HTMLElement} activeLink - Активный элемент ссылки
- */
-function setActiveNav(activeLink) {
- document.querySelectorAll('.nav-link').forEach(link => link.classList.remove('active'));
- activeLink.classList.add('active');
-}
-
-/**
- * @description Экранирует HTML-спецсимволы для предотвращения XSS.
- * @param {any} str - Входная строка
- * @returns {string} Экранированная строка
- */
-function escapeHtml(str) {
- if (!str) return '';
- return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
-}
-
-/**
- * @description Отображает всплывающее уведомление.
- * @param {string} message - Текст уведомления
- * @param {string} type - Тип уведомления (success, error, warning, info)
- */
-function showNotification(message, type = 'info') {
- const container = document.getElementById('notificationContainer');
- const notification = document.createElement('div');
- notification.className = `notification ${type}`;
- const icons = { success: '', error: '', warning: '', info: '' };
- notification.innerHTML = `${icons[type] || icons.info}${escapeHtml(message)}`;
- container.appendChild(notification);
- setTimeout(() => { notification.style.animation = 'slideOutRight 0.3s ease'; setTimeout(() => notification.remove(), 300); }, 3000);
-}
-
-/**
- * @description Сохраняет настройки интерфейса (тема).
- */
-function saveSettings() {
- const theme = document.getElementById('themeSelect')?.value;
- if (theme) { localStorage.setItem('theme', theme); showNotification('Настройки сохранены', 'success'); }
-}
-
-// ============================== ТРАНЗАКЦИИ (ДОПОЛНИТЕЛЬНЫЕ ФУНКЦИИ) ==============================
-
-/**
- * @async
- * @description Загружает детали транзакции в модальном окне.
- * @param {string} txId - ID транзакции
- */
-async function loadTransactionDetails(txId) {
- modalTitle.textContent = `Детали транзакции ${txId}`;
- modalConfirm.textContent = 'Закрыть';
- modalBody.innerHTML = '';
- modal.classList.add('show');
-
- const originalConfirmHandler = modalConfirm.onclick;
- modalConfirm.onclick = () => { modal.classList.remove('show'); modalConfirm.onclick = originalConfirmHandler; };
-
- try {
- const response = await fetch(`/api/webui/transaction/${txId}/details`);
- const data = await response.json();
- if (data.success && data.data) {
- const tx = data.data;
- modalBody.innerHTML = `ID: ${escapeHtml(tx.id)}
Статус: ${escapeHtml(tx.status)}
Время начала: ${new Date(tx.start_time).toLocaleString()}
Количество операций: ${tx.operation_count}
Операции (${tx.operations?.length || 0})
${tx.operations?.length ? `
| Тип | БД | Коллекция | ID документа |
${tx.operations.map(op => `| ${escapeHtml(op.type)} | ${escapeHtml(op.database)} | ${escapeHtml(op.collection)} | ${escapeHtml(op.document_id)} |
`).join('')}
` : '
Нет операций
'}
`;
- } else {
- modalBody.innerHTML = `Ошибка загрузки деталей: ${data.error || 'Неизвестная ошибка'}
`;
- }
- } catch (error) {
- modalBody.innerHTML = 'Ошибка подключения
';
- }
-}
-
-/**
- * @async
- * @description Начинает новую сессию транзакций.
- */
-async function startSession() {
- try {
- const response = await fetch('/api/webui/transactions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'start_session' }) });
- const data = await response.json();
- data.success ? (showNotification('Сессия начата', 'success'), loadTransactionList()) : showNotification(data.error || 'Ошибка', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-}
-
-/**
- * @async
- * @description Начинает новую транзакцию.
- */
-async function startTransaction() {
- try {
- const response = await fetch('/api/webui/transactions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'start_transaction' }) });
- const data = await response.json();
- data.success ? (showNotification('Транзакция начата', 'success'), loadTransactionList()) : showNotification(data.error || 'Ошибка', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-}
-
-/**
- * @async
- * @description Фиксирует текущую транзакцию.
- */
-async function commitTransaction() {
- try {
- const response = await fetch('/api/webui/transaction/commit', { method: 'POST' });
- const data = await response.json();
- data.success ? (showNotification('Транзакция зафиксирована', 'success'), loadTransactionList()) : showNotification(data.error || 'Ошибка', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-}
-
-/**
- * @async
- * @description Отменяет текущую транзакцию.
- */
-async function abortTransaction() {
- try {
- const response = await fetch('/api/webui/transaction/abort', { method: 'POST' });
- const data = await response.json();
- data.success ? (showNotification('Транзакция отменена', 'success'), loadTransactionList()) : showNotification(data.error || 'Ошибка', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-}
-
-/**
- * @async
- * @description Фиксирует транзакцию по ID.
- * @param {string} txId - ID транзакции
- */
-async function commitTransactionById(txId) {
- if (!confirm(`Зафиксировать транзакцию ${txId}?`)) return;
- try {
- const response = await fetch(`/api/webui/transaction/${txId}/commit`, { method: 'POST' });
- const data = await response.json();
- data.success ? (showNotification(`Транзакция ${txId} зафиксирована`, 'success'), loadTransactionList()) : showNotification(data.error || 'Ошибка фиксации', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-}
-
-/**
- * @async
- * @description Отменяет транзакцию по ID.
- * @param {string} txId - ID транзакции
- */
-async function abortTransactionById(txId) {
- if (!confirm(`Отменить транзакцию ${txId}?`)) return;
- try {
- const response = await fetch(`/api/webui/transaction/${txId}/abort`, { method: 'POST' });
- const data = await response.json();
- data.success ? (showNotification(`Транзакция ${txId} отменена`, 'success'), loadTransactionList()) : showNotification(data.error || 'Ошибка отмены', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-}
-
-// ============================== УПРАВЛЕНИЕ ДАННЫМИ (CRUD) ==============================
-
-/**
- * @async
- * @description Удаляет коллекцию.
- * @param {string} dbName - Имя БД
- * @param {string} collName - Имя коллекции
- */
-window.deleteCollection = async function(dbName, collName) {
- if (!confirm(`Удалить коллекцию "${collName}"? Это действие необратимо.`)) return;
- try {
- const response = await fetch(`/api/db/${dbName}/${collName}`, { method: 'DELETE' });
- response.ok ? (showNotification(`Коллекция "${collName}" удалена`, 'success'), viewDatabase(dbName)) : showNotification((await response.json()).error || 'Ошибка удаления коллекции', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-};
-
-/**
- * @async
- * @description Удаляет документ.
- * @param {string} dbName - Имя БД
- * @param {string} collName - Имя коллекции
- * @param {string} docId - ID документа
- */
-window.deleteDocument = async function(dbName, collName, docId) {
- if (!confirm(`Удалить документ "${docId}"?`)) return;
- try {
- const response = await fetch(`/api/webui/documents/${dbName}/${collName}?id=${encodeURIComponent(docId)}`, { method: 'DELETE' });
- const result = await response.json();
- result.success ? (showNotification('Документ удалён', 'success'), viewCollection(dbName, collName)) : showNotification(result.error || 'Ошибка удаления документа', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-};
-
-/**
- * @async
- * @description Удаляет пользователя.
- * @param {string} username - Имя пользователя
- */
-window.deleteUser = async function(username) {
- if (!confirm(`Удалить пользователя "${username}"?`)) return;
- try {
- const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, { method: 'DELETE' });
- const data = await response.json();
- data.success ? (showNotification(`Пользователь ${username} удалён`, 'success'), loadACLUsers()) : showNotification(data.error || 'Ошибка удаления', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-};
-
-/**
- * @async
- * @description Удаляет роль.
- * @param {string} roleName - Имя роли
- */
-window.deleteRole = async function(roleName) {
- if (!confirm(`Удалить роль "${roleName}"?`)) return;
- try {
- const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}`, { method: 'DELETE' });
- const data = await response.json();
- data.success ? (showNotification(`Роль ${roleName} удалена`, 'success'), loadACLRoles()) : showNotification(data.error || 'Ошибка удаления', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-};
-
-/**
- * @async
- * @description Отключает пользователя.
- * @param {string} username - Имя пользователя
- */
-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();
- data.success ? (showNotification(`Пользователь ${username} отключён`, 'success'), loadACLUsers()) : showNotification(data.error || 'Ошибка', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-};
-
-/**
- * @async
- * @description Включает пользователя.
- * @param {string} username - Имя пользователя
- */
-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();
- data.success ? (showNotification(`Пользователь ${username} включён`, 'success'), loadACLUsers()) : showNotification(data.error || 'Ошибка', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-};
-
-/**
- * @async
- * @description Отзывает разрешение у роли.
- * @param {string} roleName - Имя роли
- * @param {string} permission - Разрешение
- */
-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();
- data.success ? (showNotification('Разрешение отозвано', 'success'), loadACLRoles()) : showNotification(data.error || 'Ошибка', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-};
-
-/**
- * @async
- * @description Удаляет индекс.
- * @param {string} dbName - Имя БД
- * @param {string} collName - Имя коллекции
- * @param {string} indexName - Имя индекса
- */
-window.dropIndex = async function(dbName, collName, indexName) {
- if (!confirm(`Удалить индекс "${indexName}"?`)) return;
- try {
- const response = await fetch(`/api/webui/index/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/drop/${encodeURIComponent(indexName)}`, { method: 'POST' });
- const data = await response.json();
- data.success ? (showNotification(`Индекс ${indexName} удалён`, 'success'), loadIndexesForCollection()) : showNotification(data.error || 'Ошибка удаления индекса', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-};
-
-/**
- * @async
- * @description Включает или отключает триггер.
- * @param {string} dbName - Имя БД
- * @param {string} collName - Имя коллекции
- * @param {string} triggerName - Имя триггера
- * @param {string} triggerEvent - Событие триггера
- * @param {boolean} enable - Включить (true) или отключить (false)
- */
-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();
- data.success ? (showNotification(`Триггер ${triggerName} ${enable ? 'включён' : 'отключён'}`, 'success'), loadTriggersForCollection()) : showNotification(data.error || 'Ошибка', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-};
-
-/**
- * @async
- * @description Удаляет триггер.
- * @param {string} dbName - Имя БД
- * @param {string} collName - Имя коллекции
- * @param {string} triggerName - Имя триггера
- * @param {string} triggerEvent - Событие триггера
- */
-window.deleteTrigger = async function(dbName, collName, triggerName, triggerEvent) {
- if (!confirm(`Удалить триггер "${triggerName}"?`)) return;
- try {
- const response = await fetch(`/api/webui/trigger/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/delete/${encodeURIComponent(triggerName)}/${encodeURIComponent(triggerEvent)}`, { method: 'DELETE' });
- const data = await response.json();
- data.success ? (showNotification(`Триггер ${triggerName} удалён`, 'success'), loadTriggersForCollection()) : showNotification(data.error || 'Ошибка удаления', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-};
-
-/**
- * @async
- * @description Удаляет ограничение.
- * @param {string} dbName - Имя БД
- * @param {string} collName - Имя коллекции
- * @param {string} constraintType - Тип ограничения
- * @param {string} field - Имя поля
- */
-window.removeConstraint = async function(dbName, collName, constraintType, field) {
- const confirmMsg = { required: `Удалить обязательное поле "${field}"?`, unique: `Удалить уникальное ограничение для поля "${field}"?`, min: `Удалить минимальное значение для поля "${field}"?`, max: `Удалить максимальное значение для поля "${field}"?`, enum: `Удалить перечисление для поля "${field}"?`, regex: `Удалить регулярное выражение для поля "${field}"?` }[constraintType] || `Удалить ограничение "${field}"?`;
- if (!confirm(confirmMsg)) return;
- try {
- const response = await fetch(`/api/webui/constraint/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/${constraintType}/${encodeURIComponent(field)}`, { method: 'DELETE' });
- const data = await response.json();
- data.success ? (showNotification('Ограничение удалено', 'success'), loadConstraintsForCollection()) : showNotification(data.error || 'Ошибка удаления', 'error');
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
-};
-
-/**
- * @description Обрабатывает быстрые действия из меню.
- * @param {string} action - Идентификатор действия (data-action)
- */
-function handleCrudAction(action) {
- const actions = {
- 'create-db': showCreateDatabaseModal,
- 'create-collection': showCreateCollectionModal,
- 'insert-doc': showInsertDocumentModal,
- 'find-doc': () => showUpdateDocumentModal(),
- 'update-doc': () => showUpdateDocumentModal(),
- 'delete-doc': () => showDeleteDocumentModal(),
- 'acl-create-user': showCreateUserModal,
- 'acl-create-role': showCreateRoleModal,
- 'tx-start-session': startSession,
- 'tx-start': startTransaction,
- 'tx-commit': commitTransaction,
- 'tx-abort': abortTransaction,
- 'index-create': () => { if (document.getElementById('indexDbSelect')?.value && document.getElementById('indexCollSelect')?.value) showCreateIndexModal(); else showNotification('Сначала выберите БД и коллекцию на странице индексов', 'warning'); },
- 'constraint-add-required': showAddRequiredConstraintModal,
- 'constraint-add-unique': showAddUniqueConstraintModal,
- 'constraint-add-min': showAddMinConstraintModal,
- 'constraint-add-max': showAddMaxConstraintModal,
- 'constraint-add-enum': showAddEnumConstraintModal,
- 'constraint-add-regex': showAddRegexConstraintModal
- };
- (actions[action] || (() => showNotification('Неизвестное действие', 'warning')))();
-}
-
-// ============================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ДЛЯ ОГРАНИЧЕНИЙ ==============================
-
-function showAddRequiredConstraintModal() { showConstraintModal('required', 'Добавить обязательное поле', [{ name: 'field', label: 'Имя поля', type: 'text' }]); }
-function showAddUniqueConstraintModal() { showConstraintModal('unique', 'Добавить уникальное поле', [{ name: 'field', label: 'Имя поля', type: 'text' }]); }
-function showAddMinConstraintModal() { showConstraintModal('min', 'Добавить минимальное значение', [{ name: 'field', label: 'Имя поля', type: 'text' }, { name: 'value', label: 'Минимальное значение', type: 'number' }]); }
-function showAddMaxConstraintModal() { showConstraintModal('max', 'Добавить максимальное значение', [{ name: 'field', label: 'Имя поля', type: 'text' }, { name: 'value', label: 'Максимальное значение', type: 'number' }]); }
-function showAddEnumConstraintModal() { showConstraintModal('enum', 'Добавить перечисление (Enum)', [{ name: 'field', label: 'Имя поля', type: 'text' }, { name: 'values', label: 'Допустимые значения (через запятую)', type: 'text' }]); }
-function showAddRegexConstraintModal() { showConstraintModal('regex', 'Добавить регулярное выражение', [{ name: 'field', label: 'Имя поля', type: 'text' }, { name: 'pattern', label: 'Регулярное выражение', type: 'text' }]); }
-
-function showConstraintModal(type, title, fields) {
- const dbName = document.getElementById('constraintDbSelect')?.value;
- const collName = document.getElementById('constraintCollSelect')?.value;
- if (!dbName || !collName) { showNotification('Сначала выберите БД и коллекцию на странице ограничений', 'warning'); return; }
- modalTitle.textContent = title;
- modalConfirm.textContent = 'Добавить';
- modalBody.innerHTML = `${fields.map(f => ``).join('')}`;
- modal.classList.add('show');
- modalConfirm.onclick = async () => {
- const field = document.getElementById('constraint_field')?.value;
- if (!field) { showNotification('Введите имя поля', 'error'); return; }
- let url = `/api/webui/constraint/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/${type}/${encodeURIComponent(field)}`;
- if (type === 'min' || type === 'max') {
- const value = document.getElementById('constraint_value')?.value;
- if (value === '') { showNotification('Введите значение', 'error'); return; }
- url += `/${encodeURIComponent(value)}`;
- } else if (type === 'enum') {
- const valuesStr = document.getElementById('constraint_values')?.value;
- if (!valuesStr) { showNotification('Введите допустимые значения', 'error'); return; }
- const values = valuesStr.split(',').map(v => encodeURIComponent(v.trim()));
- url += `/${values.join('/')}`;
- } else if (type === 'regex') {
- const pattern = document.getElementById('constraint_pattern')?.value;
- if (!pattern) { showNotification('Введите регулярное выражение', 'error'); return; }
- url += `/${encodeURIComponent(pattern)}`;
- }
- try {
- const response = await fetch(url, { method: 'POST' });
- const data = await response.json();
- if (data.success) {
- modal.classList.remove('show');
- showNotification(`Ограничение добавлено`, 'success');
- loadConstraintsForCollection();
- } else {
- showNotification(data.error || 'Ошибка добавления', 'error');
- }
- } catch (error) { showNotification('Ошибка подключения', 'error'); }
- };
-}