diff --git a/internal/api/static/app.js b/internal/api/static/app.js
deleted file mode 100644
index 682aa1f..0000000
--- a/internal/api/static/app.js
+++ /dev/null
@@ -1,1413 +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
- */
-
-/**
- * Futriis DB Dashboard - Web Interface JavaScript для веб-интерфейса Futriis DB Dashboard
- * @version 1.0.0
- * @description Обеспечивает полное управление СУБД: CRUD операции, ACL, индексы,
- * транзакции, триггеры, ограничения (constraints), импорт/экспорт,
- * управление кластером и аудит. Использует async/await, Fetch API,
- * динамическую отрисовку DOM и модальные окна.
- * @author Futriis Team
- * @license CDDL-1.0
- */
-
-// ======================== СОСТОЯНИЕ ПРИЛОЖЕНИЯ ========================
-
-/** @type {string|null} ID текущей сессии */
-let currentSession = null;
-
-/** @type {string|null} Имя текущей базы данных */
-let currentDatabase = null;
-
-/** @type {string|null} Имя текущей коллекции */
-let currentCollection = null;
-
-/** @type {string|null} Имя текущего пользователя */
-let currentUser = null;
-
-/** @type {boolean} Флаг администратора */
-let isAdmin = false;
-
-// ======================== DOM ЭЛЕМЕНТЫ ========================
-
-const DOM = {
- content: document.getElementById('contentArea'),
- title: document.getElementById('pageTitle'),
- connection: document.getElementById('connectionStatus'),
- userName: document.querySelector('#userName'),
- userRole: document.getElementById('userRole'),
- logoutBtn: document.getElementById('logoutBtn'),
- menuToggle: document.getElementById('menuToggle'),
- sidebar: document.querySelector('.sidebar'),
- modal: document.getElementById('modal'),
- modalTitle: document.getElementById('modalTitle'),
- modalBody: document.getElementById('modalBody'),
- modalConfirm: document.getElementById('modalConfirm'),
- modalCloseBtns: document.querySelectorAll('.modal-close'),
- changePasswordIcon: document.getElementById('changePasswordIcon'),
- userAvatar: document.getElementById('userAvatar'),
- notificationContainer: document.getElementById('notificationContainer')
-};
-
-// ======================== ИНИЦИАЛИЗАЦИЯ ========================
-
-/**
- * Главная точка входа. Выполняется после загрузки DOM.
- */
-document.addEventListener('DOMContentLoaded', async () => {
- await checkSession();
- initNavigation();
- initEventListeners();
- initAvatarUpload();
- initChangePassword();
-});
-
-// ======================== АУТЕНТИФИКАЦИЯ ========================
-
-/**
- * Проверяет активность сессии на сервере
- */
-async function checkSession() {
- try {
- const response = await fetch('/api/webui/session');
- const data = await response.json();
-
- if (data.success && data.data.authenticated) {
- currentUser = data.data.username;
- isAdmin = data.data.is_admin || false;
- DOM.userName.textContent = currentUser;
- DOM.userRole.textContent = isAdmin ? 'Администратор' : 'Пользователь';
-
- if (data.data.avatar) {
- updateAvatarDisplay(data.data.avatar);
- } else {
- loadUserAvatar();
- }
-
- updateConnectionStatus(data.data.connection_status);
- loadDashboard();
- startConnectionStatusMonitor();
- } else {
- showLoginModal();
- }
- } catch (error) {
- console.error('Session check failed:', error);
- showLoginModal();
- }
-}
-
-/**
- * Запускает периодическую проверку статуса подключения
- */
-function startConnectionStatusMonitor() {
- setInterval(async () => {
- if (currentUser) {
- try {
- const response = await fetch('/api/webui/session');
- const data = await response.json();
- updateConnectionStatus(data.data?.connection_status || 'disconnected');
- } catch (error) {
- updateConnectionStatus('disconnected');
- }
- }
- }, 5000);
-}
-
-/**
- * Обновляет индикатор подключения
- * @param {string} status - Статус подключения ('connected' или 'disconnected')
- */
-function updateConnectionStatus(status) {
- if (status === 'connected') {
- DOM.connection.className = 'connection-status online';
- DOM.connection.innerHTML = 'СУБД подключена';
- } else {
- DOM.connection.className = 'connection-status offline';
- DOM.connection.innerHTML = 'СУБД не подключена';
- }
-}
-
-/**
- * Отображает модальное окно для входа в систему
- */
-function showLoginModal() {
- DOM.modalTitle.textContent = 'Вход в систему СУБД Futriis';
- DOM.modalBody.innerHTML = `
-
-
-
-
-
-
-
-
- `;
- DOM.modalConfirm.textContent = 'Войти';
- DOM.modal.classList.add('show');
-
- const confirmHandler = async () => {
- const username = document.getElementById('username').value;
- const password = document.getElementById('password').value;
-
- if (!username || !password) {
- showNotification('Пожалуйста, заполните все поля', 'error');
- return;
- }
-
- try {
- const response = await fetch('/api/webui/login', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ username, password })
- });
- const data = await response.json();
-
- if (data.success) {
- currentUser = username;
- isAdmin = data.data.is_admin || false;
- DOM.userName.textContent = username;
- DOM.userRole.textContent = isAdmin ? 'Администратор' : 'Пользователь';
-
- if (data.data.avatar) {
- updateAvatarDisplay(data.data.avatar);
- }
-
- DOM.modal.classList.remove('show');
- showNotification('Вход выполнен успешно', 'success');
- updateConnectionStatus('connected');
- startConnectionStatusMonitor();
- loadDashboard();
- } else {
- showNotification(data.error || 'Неверный логин и/или пароль', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения к серверу', 'error');
- }
- };
-
- DOM.modalConfirm.onclick = confirmHandler;
-
- const handleEnter = (e) => {
- if (e.key === 'Enter') {
- confirmHandler();
- document.removeEventListener('keydown', handleEnter);
- }
- };
- document.addEventListener('keydown', handleEnter);
-}
-
-// ======================== АВАТАР ПОЛЬЗОВАТЕЛЯ ========================
-
-/**
- * Загружает аватар пользователя с сервера
- */
-async function loadUserAvatar() {
- try {
- const response = await fetch('/api/webui/user/info');
- const data = await response.json();
-
- if (data.success && data.data.avatar) {
- updateAvatarDisplay(data.data.avatar);
- }
- } catch (error) {
- console.error('Failed to load avatar:', error);
- }
-}
-
-/**
- * Обновляет отображение аватара
- * @param {string} avatarBase64 - Аватар в формате base64
- */
-function updateAvatarDisplay(avatarBase64) {
- if (!DOM.userAvatar) return;
- DOM.userAvatar.innerHTML = `
`;
-}
-
-/**
- * Инициализирует загрузку аватара
- */
-function initAvatarUpload() {
- if (!DOM.userAvatar) return;
-
- DOM.userAvatar.style.cursor = 'pointer';
- DOM.userAvatar.addEventListener('click', () => {
- showAvatarUploadModal();
- });
-}
-
-/**
- * Показывает модальное окно загрузки аватара
- */
-function showAvatarUploadModal() {
- const avatarModal = document.getElementById('avatarUploadModal');
- const fileInput = document.getElementById('avatarFile');
- const preview = document.getElementById('avatarPreview');
- const uploadBtn = document.getElementById('uploadAvatarBtn');
-
- if (!avatarModal) return;
-
- if (fileInput) fileInput.value = '';
- if (preview) preview.innerHTML = '';
-
- avatarModal.classList.add('show');
-
- if (fileInput) {
- fileInput.onchange = function() {
- if (this.files && this.files[0]) {
- const reader = new FileReader();
- reader.onload = function(e) {
- if (preview) {
- preview.innerHTML = `
`;
- }
- };
- reader.readAsDataURL(this.files[0]);
- }
- };
- }
-
- if (uploadBtn) {
- uploadBtn.onclick = async () => {
- if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
- showNotification('Выберите изображение', 'warning');
- return;
- }
-
- const formData = new FormData();
- formData.append('avatar', fileInput.files[0]);
-
- try {
- const response = await fetch('/api/webui/user/avatar', {
- method: 'POST',
- body: formData
- });
- const data = await response.json();
-
- if (data.success) {
- updateAvatarDisplay(data.data.avatar);
- avatarModal.classList.remove('show');
- showNotification('Аватар успешно загружен', 'success');
- } else {
- showNotification(data.error || 'Ошибка загрузки аватара', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
- };
- }
-
- const closeButtons = avatarModal.querySelectorAll('.modal-close');
- closeButtons.forEach(btn => {
- btn.onclick = () => {
- avatarModal.classList.remove('show');
- };
- });
-}
-
-// ======================== СМЕНА ПАРОЛЯ ========================
-
-/**
- * Инициализирует смену пароля
- */
-function initChangePassword() {
- if (DOM.changePasswordIcon) {
- DOM.changePasswordIcon.addEventListener('click', () => {
- showChangePasswordModal();
- });
- }
-}
-
-/**
- * Показывает модальное окно смены пароля
- */
-function showChangePasswordModal() {
- const passwordModal = document.getElementById('changePasswordModal');
- const currentPasswordInput = document.getElementById('currentPassword');
- const newPasswordInput = document.getElementById('newPassword');
- const confirmPasswordInput = document.getElementById('confirmPassword');
- const changeBtn = document.getElementById('changePasswordBtn');
-
- if (!passwordModal) return;
-
- if (currentPasswordInput) currentPasswordInput.value = '';
- if (newPasswordInput) newPasswordInput.value = '';
- if (confirmPasswordInput) confirmPasswordInput.value = '';
-
- passwordModal.classList.add('show');
-
- if (changeBtn) {
- changeBtn.onclick = async () => {
- const currentPassword = currentPasswordInput?.value || '';
- const newPassword = newPasswordInput?.value || '';
- const confirmPassword = confirmPasswordInput?.value || '';
-
- if (!currentPassword) {
- showNotification('Введите текущий пароль', 'warning');
- return;
- }
-
- if (!newPassword) {
- showNotification('Введите новый пароль', 'warning');
- return;
- }
-
- if (newPassword !== confirmPassword) {
- showNotification('Новый пароль и подтверждение не совпадают', 'error');
- return;
- }
-
- if (newPassword.length < 4) {
- showNotification('Новый пароль должен содержать минимум 4 символа', 'error');
- return;
- }
-
- try {
- const response = await fetch('/api/webui/change-password', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- current_password: currentPassword,
- new_password: newPassword
- })
- });
- const data = await response.json();
-
- if (data.success) {
- passwordModal.classList.remove('show');
- showNotification('Пароль успешно изменён', 'success');
- } else {
- showNotification(data.error || 'Ошибка смены пароля', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
- };
- }
-
- const closeButtons = passwordModal.querySelectorAll('.modal-close');
- closeButtons.forEach(btn => {
- btn.onclick = () => {
- passwordModal.classList.remove('show');
- };
- });
-}
-
-// ======================== НАВИГАЦИЯ ========================
-
-/**
- * Инициализирует обработчики навигации
- */
-function initNavigation() {
- document.querySelectorAll('.nav-link[data-section]').forEach(link => {
- link.addEventListener('click', (e) => {
- e.preventDefault();
- const section = link.dataset.section;
- loadSection(section);
- setActiveNav(link);
- });
- });
-
- document.querySelectorAll('[data-action]').forEach(item => {
- item.addEventListener('click', (e) => {
- e.preventDefault();
- const action = item.dataset.action;
- handleAction(action);
- });
- });
-
- document.querySelectorAll('.has-submenu > .nav-link').forEach(link => {
- link.addEventListener('click', (e) => {
- e.preventDefault();
- const parent = link.closest('.has-submenu');
- parent.classList.toggle('open');
- });
- });
-}
-
-/**
- * Инициализирует глобальные обработчики событий
- */
-function initEventListeners() {
- DOM.logoutBtn.addEventListener('click', async () => {
- await fetch('/api/webui/logout', { method: 'POST' });
- currentUser = null;
- isAdmin = false;
- updateConnectionStatus('disconnected');
- if (DOM.userAvatar) {
- DOM.userAvatar.innerHTML = '';
- }
- showLoginModal();
- });
-
- if (DOM.menuToggle) {
- DOM.menuToggle.addEventListener('click', () => {
- DOM.sidebar.classList.toggle('open');
- });
- }
-
- DOM.modalCloseBtns.forEach(btn => {
- btn.addEventListener('click', () => {
- DOM.modal.classList.remove('show');
- });
- });
-
- DOM.modal.addEventListener('click', (e) => {
- if (e.target === DOM.modal) {
- DOM.modal.classList.remove('show');
- }
- });
-}
-
-/**
- * Загружает соответствующую секцию интерфейса
- * @param {string} section - Идентификатор секции
- */
-async function loadSection(section) {
- const sections = {
- dashboard: loadDashboard,
- cluster: loadClusterManagement,
- logs: loadLogs,
- 'plugins-list': loadPluginsList,
- settings: loadSettings,
- 'acl-users': loadACLUsers,
- 'acl-roles': loadACLRoles,
- 'acl-permissions': loadACLPermissions,
- 'tx-list': loadTransactionList,
- 'indexes-list': loadIndexesList,
- 'export-data': loadExportPage,
- 'import-data': loadImportPage,
- 'constraints-list': loadConstraintsList,
- 'triggers-list': loadTriggersList,
- 'trigger-log': loadTriggerLog
- };
-
- const loader = sections[section];
- if (loader) {
- await loader();
- } else {
- DOM.content.innerHTML = 'Раздел в разработке
';
- }
-}
-
-/**
- * Устанавливает активный пункт навигации
- * @param {HTMLElement} activeLink - Активный элемент ссылки
- */
-function setActiveNav(activeLink) {
- document.querySelectorAll('.nav-link').forEach(link => link.classList.remove('active'));
- activeLink.classList.add('active');
-}
-
-// ======================== ДАШБОРД ========================
-
-/**
- * Загружает и отображает главную панель управления
- */
-async function loadDashboard() {
- DOM.title.textContent = 'Панель управления';
- DOM.content.innerHTML = '';
-
- try {
- const [statsRes, dbsRes] = await Promise.all([
- fetch('/api/webui/stats'),
- fetch('/api/webui/databases')
- ]);
- const stats = await statsRes.json();
- const databases = await dbsRes.json();
-
- DOM.content.innerHTML = `
-
-
${stats.data.databases || 0}
Базы данных
-
${stats.data.collections || 0}
Коллекции
-
${stats.data.documents || 0}
Документы
-
${stats.data.storage_used_mb?.toFixed(2) || 0} MB
Использовано памяти
-
- Базы данных
-
| Имя БД | Коллекции | Действия |
- ${databases.data.map(db => `| ${escapeHtml(db.name)} | ${db.collections} | |
`).join('')}
-
- `;
- } catch (error) {
- DOM.content.innerHTML = 'Ошибка загрузки данных
';
- showNotification('Ошибка загрузки дашборда', 'error');
- }
-}
-
-// ======================== БАЗЫ ДАННЫХ И КОЛЛЕКЦИИ ========================
-
-/**
- * Отображает список коллекций в выбранной базе данных
- * @param {string} dbName - Имя базы данных
- */
-window.viewDatabase = async function(dbName) {
- currentDatabase = dbName;
- DOM.title.textContent = `База данных: ${dbName}`;
- DOM.content.innerHTML = '';
-
- try {
- const response = await fetch(`/api/webui/collections/${dbName}`);
- const data = await response.json();
-
- if (data.success) {
- DOM.content.innerHTML = `
-
-
-
Коллекции
-
-
-
| Имя коллекции | Документов | Размер | Индексы | Действия |
- ${data.data.collections.map(coll => `
-
- | ${escapeHtml(coll.name)} |
- ${coll.count} |
- ${(coll.size / 1024).toFixed(2)} KB |
- ${coll.indexes.length} |
-
-
-
- |
-
- `).join('')}
-
-
- `;
- } else {
- DOM.content.innerHTML = 'Ошибка загрузки коллекций
';
- }
- } catch (error) {
- DOM.content.innerHTML = 'Ошибка подключения
';
- }
-};
-
-/**
- * Отображает документы выбранной коллекции
- * @param {string} dbName - Имя базы данных
- * @param {string} collName - Имя коллекции
- */
-window.viewCollection = async function(dbName, collName) {
- currentDatabase = dbName;
- currentCollection = collName;
- DOM.title.textContent = `Коллекция: ${dbName}.${collName}`;
- DOM.content.innerHTML = '';
-
- try {
- const response = await fetch(`/api/webui/documents/${dbName}/${collName}?limit=100`);
- const data = await response.json();
-
- if (data.success) {
- DOM.content.innerHTML = `
-
-
-
-
-
-
Документы (${data.data.total} всего)
-
| ID | Поля | Создан | Действия |
- ${data.data.documents.map(doc => `
-
- ${escapeHtml(doc.id)} |
- ${escapeHtml(JSON.stringify(doc.fields, null, 2))} |
- ${new Date(doc.created_at).toLocaleString()} |
-
-
-
- |
-
- `).join('')}
-
-
- `;
- } else {
- DOM.content.innerHTML = 'Ошибка загрузки документов
';
- }
- } catch (error) {
- DOM.content.innerHTML = 'Ошибка подключения
';
- }
-};
-
-/**
- * Удаляет коллекцию
- * @param {string} dbName - Имя БД
- * @param {string} collName - Имя коллекции
- */
-window.deleteCollection = async function(dbName, collName) {
- if (!confirm(`Удалить коллекцию "${collName}"? Это действие необратимо.`)) return;
- try {
- const response = await fetch(`/api/db/${dbName}/${collName}`, { method: 'DELETE' });
- if (response.ok) {
- showNotification(`Коллекция "${collName}" удалена`, 'success');
- viewDatabase(dbName);
- } else {
- const error = await response.json();
- showNotification(error.error || 'Ошибка удаления коллекции', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
-};
-
-/**
- * Удаляет документ
- * @param {string} dbName - Имя БД
- * @param {string} collName - Имя коллекции
- * @param {string} docId - ID документа
- */
-window.deleteDocument = async function(dbName, collName, docId) {
- if (!confirm(`Удалить документ "${docId}"?`)) return;
- try {
- const response = await fetch(`/api/webui/documents/${dbName}/${collName}?id=${encodeURIComponent(docId)}`, { method: 'DELETE' });
- const result = await response.json();
- if (result.success) {
- showNotification('Документ удалён', 'success');
- viewCollection(dbName, collName);
- } else {
- showNotification(result.error || 'Ошибка удаления документа', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
-};
-
-// ======================== ЛОГИ ОПЕРАЦИЙ ========================
-
-/**
- * Загружает и отображает лог операций веб-интерфейса
- */
-async function loadLogs() {
- if (!isAdmin) {
- DOM.content.innerHTML = 'Доступ запрещён. Только для администраторов.
';
- return;
- }
-
- DOM.title.textContent = 'Лог операций';
- DOM.content.innerHTML = '';
-
- try {
- const response = await fetch('/api/webui/logs?limit=500');
- const data = await response.json();
-
- if (data.success) {
- const logs = data.data;
- if (logs.length === 0) {
- DOM.content.innerHTML = 'Лог операций пуст
';
- return;
- }
-
- DOM.content.innerHTML = `
-
-
Лог операций веб-интерфейса
-
-
-
-
- | Время | Операция | Цель | Пользователь | Статус | Ошибка |
-
- ${logs.map(log => `
-
- | ${new Date(log.timestamp).toLocaleString()} |
- ${escapeHtml(log.operation)} |
- ${escapeHtml(log.target)} |
- ${escapeHtml(log.user)} |
- ${log.status === 'success' ? 'Успех' : 'Ошибка'} |
- ${log.error_msg ? `${escapeHtml(log.error_msg)}` : '-'} |
-
- `).join('')}
-
-
- `;
- } else {
- DOM.content.innerHTML = 'Ошибка загрузки логов
';
- }
- } catch (error) {
- DOM.content.innerHTML = 'Ошибка подключения
';
- }
-}
-
-// ======================== ПЛАГИНЫ ========================
-
-/**
- * Загружает и отображает список плагинов
- */
-async function loadPluginsList() {
- if (!isAdmin) {
- DOM.content.innerHTML = 'Доступ запрещён. Только для администраторов.
';
- return;
- }
-
- DOM.title.textContent = 'Управление плагинами';
- DOM.content.innerHTML = '';
-
- try {
- const response = await fetch('/api/webui/plugins');
- const data = await response.json();
-
- if (data.success) {
- const plugins = data.data;
- DOM.content.innerHTML = `
-
-
-
-
-
-
Плагины
-
- | Имя | Версия | Автор | Описание | Загружен | Действия |
-
- ${plugins.map(plugin => `
-
- | ${escapeHtml(plugin.name)} |
- ${escapeHtml(plugin.version)} |
- ${escapeHtml(plugin.author)} |
- ${escapeHtml(plugin.description)} |
- ${new Date(plugin.loaded_at).toLocaleString()} |
-
-
-
-
- |
-
- `).join('')}
- ${plugins.length === 0 ? '| Нет загруженных плагинов |
' : ''}
-
-
- `;
- } else {
- DOM.content.innerHTML = 'Ошибка загрузки плагинов
';
- }
- } catch (error) {
- DOM.content.innerHTML = 'Ошибка подключения
';
- }
-}
-
-/**
- * Показывает модальное окно загрузки плагина
- */
-function showUploadPluginModal() {
- DOM.modalTitle.textContent = 'Загрузить плагин';
- DOM.modalBody.innerHTML = `
-
-
-
-
-
-
- Плагины должны быть написаны на Lua и содержать функции on_load, on_start, on_stop, on_unload
-
- `;
- DOM.modalConfirm.textContent = 'Загрузить';
- DOM.modal.classList.add('show');
-
- DOM.modalConfirm.onclick = async () => {
- const fileInput = document.getElementById('pluginFile');
- if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
- showNotification('Выберите файл плагина', 'warning');
- return;
- }
-
- const formData = new FormData();
- formData.append('plugin', fileInput.files[0]);
-
- try {
- const response = await fetch('/api/webui/plugin/upload', {
- method: 'POST',
- body: formData
- });
- const data = await response.json();
-
- if (data.success) {
- DOM.modal.classList.remove('show');
- showNotification('Плагин загружен', 'success');
- loadPluginsList();
- } else {
- showNotification(data.error || 'Ошибка загрузки плагина', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
- };
-}
-
-/**
- * Запускает плагин
- * @param {string} pluginName - Имя плагина
- */
-window.startPlugin = async function(pluginName) {
- try {
- const response = await fetch(`/api/webui/plugin/${pluginName}/start`, { method: 'POST' });
- const data = await response.json();
- if (data.success) {
- showNotification(`Плагин ${pluginName} запущен`, 'success');
- loadPluginsList();
- } else {
- showNotification(data.error || 'Ошибка запуска', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
-};
-
-/**
- * Останавливает плагин
- * @param {string} pluginName - Имя плагина
- */
-window.stopPlugin = async function(pluginName) {
- try {
- const response = await fetch(`/api/webui/plugin/${pluginName}/stop`, { method: 'POST' });
- const data = await response.json();
- if (data.success) {
- showNotification(`Плагин ${pluginName} остановлен`, 'success');
- loadPluginsList();
- } else {
- showNotification(data.error || 'Ошибка остановки', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
-};
-
-/**
- * Удаляет плагин
- * @param {string} pluginName - Имя плагина
- */
-window.deletePlugin = async function(pluginName) {
- if (!confirm(`Удалить плагин "${pluginName}"?`)) return;
- try {
- const response = await fetch(`/api/webui/plugin/${pluginName}/delete`, { method: 'DELETE' });
- const data = await response.json();
- if (data.success) {
- showNotification(`Плагин ${pluginName} удалён`, 'success');
- loadPluginsList();
- } else {
- showNotification(data.error || 'Ошибка удаления', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
-};
-
-// ======================== ОСТАЛЬНЫЕ СЕКЦИИ ========================
-
-/**
- * Загружает страницу управления кластером
- */
-async function loadClusterManagement() {
- DOM.title.textContent = 'Управление кластером';
- DOM.content.innerHTML = 'Загрузка информации о кластере...
';
-
- try {
- const [statusRes, nodesRes] = await Promise.all([
- fetch('/api/webui/cluster/status'),
- fetch('/api/webui/cluster/nodes')
- ]);
- const status = await statusRes.json();
- const nodes = await nodesRes.json();
-
- DOM.content.innerHTML = `
-
-
${status.data.health === 'healthy' ? 'Здоров' : status.data.health === 'degraded' ? 'Деградирован' : 'Критический'}
Состояние кластера
-
${status.data.active_nodes}/${status.data.total_nodes}
Активные узлы
-
${status.data.replication_factor}
Фактор репликации
-
- Узлы кластера
| ID узла | Адрес | Статус | Последний контакт |
- ${nodes.data.map(node => `${escapeHtml(node.id)} | ${escapeHtml(node.ip)}:${node.port} | ${node.status} | ${new Date(node.last_seen * 1000).toLocaleString()} |
`).join('')}
-
- `;
- } catch (error) {
- DOM.content.innerHTML = 'Ошибка загрузки информации о кластере
';
- }
-}
-
-/**
- * Загружает страницу настроек
- */
-function loadSettings() {
- DOM.title.textContent = 'Настройки';
- DOM.content.innerHTML = `
- Настройки интерфейса
-
-
- `;
-}
-
-/**
- * Сохраняет настройки интерфейса
- */
-function saveSettings() {
- const theme = document.getElementById('themeSelect')?.value;
- if (theme) {
- localStorage.setItem('theme', theme);
- showNotification('Настройки сохранены', 'success');
- }
-}
-
-// ======================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ========================
-
-/**
- * Экранирует HTML-спецсимволы для предотвращения XSS
- * @param {any} str - Входная строка
- * @returns {string} Экранированная строка
- */
-function escapeHtml(str) {
- if (!str) return '';
- return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
-}
-
-/**
- * Отображает всплывающее уведомление
- * @param {string} message - Текст уведомления
- * @param {string} type - Тип уведомления (success, error, warning, info)
- */
-function showNotification(message, type = 'info') {
- const container = DOM.notificationContainer || document.getElementById('notificationContainer');
- if (!container) return;
-
- const notification = document.createElement('div');
- notification.className = `notification ${type}`;
- const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' };
- notification.innerHTML = `${icons[type] || icons.info} ${escapeHtml(message)}`;
- container.appendChild(notification);
- setTimeout(() => {
- notification.style.animation = 'slideOutRight 0.3s ease';
- setTimeout(() => notification.remove(), 300);
- }, 3000);
-}
-
-/**
- * Обрабатывает быстрые действия из меню
- * @param {string} action - Идентификатор действия
- */
-function handleAction(action) {
- const actions = {
- 'create-db': showCreateDatabaseModal,
- 'create-collection': showCreateCollectionModal,
- 'insert-doc': showInsertDocumentModal,
- 'update-doc': () => showUpdateDocumentModal(),
- 'acl-create-user': showCreateUserModal,
- 'acl-create-role': showCreateRoleModal,
- 'tx-start-session': startSession,
- 'tx-start': startTransaction,
- 'tx-commit': commitTransaction,
- 'tx-abort': abortTransaction,
- 'index-create': () => {
- if (document.getElementById('indexDbSelect')?.value && document.getElementById('indexCollSelect')?.value) {
- showCreateIndexModal();
- } else {
- showNotification('Сначала выберите БД и коллекцию на странице индексов', 'warning');
- }
- },
- 'plugin-upload': showUploadPluginModal,
- 'constraint-add-required': () => showConstraintModal('required'),
- 'constraint-add-unique': () => showConstraintModal('unique'),
- 'constraint-add-min': () => showConstraintModal('min'),
- 'constraint-add-max': () => showConstraintModal('max'),
- 'constraint-add-enum': () => showConstraintModal('enum'),
- 'constraint-add-regex': () => showConstraintModal('regex')
- };
-
- const handler = actions[action];
- if (handler) {
- handler();
- } else {
- showNotification('Неизвестное действие', 'warning');
- }
-}
-
-// ======================== МОДАЛЬНЫЕ ОКНА ДЛЯ CRUD ========================
-
-/**
- * Показывает модальное окно создания базы данных
- */
-function showCreateDatabaseModal() {
- DOM.modalTitle.textContent = 'Создать базу данных';
- DOM.modalBody.innerHTML = ``;
- DOM.modalConfirm.textContent = 'Создать';
- DOM.modal.classList.add('show');
-
- DOM.modalConfirm.onclick = async () => {
- const dbName = document.getElementById('dbName').value;
- if (!dbName) {
- showNotification('Введите имя базы данных', 'error');
- return;
- }
- try {
- const response = await fetch('/api/db/' + dbName, { method: 'POST' });
- if (response.ok) {
- DOM.modal.classList.remove('show');
- showNotification(`База данных "${dbName}" создана`, 'success');
- loadDashboard();
- } else {
- const error = await response.json();
- showNotification(error.error || 'Ошибка создания БД', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
- };
-}
-
-/**
- * Показывает модальное окно создания коллекции
- */
-function showCreateCollectionModal() {
- if (!currentDatabase) {
- showNotification('Сначала выберите базу данных', 'warning');
- return;
- }
- DOM.modalTitle.textContent = 'Создать коллекцию';
- DOM.modalBody.innerHTML = `
-
-
- `;
- DOM.modalConfirm.textContent = 'Создать';
- DOM.modal.classList.add('show');
-
- DOM.modalConfirm.onclick = async () => {
- const collName = document.getElementById('collName').value;
- if (!collName) {
- showNotification('Введите имя коллекции', 'error');
- return;
- }
- try {
- const response = await fetch(`/api/db/${currentDatabase}/${collName}`, { method: 'POST' });
- if (response.ok) {
- DOM.modal.classList.remove('show');
- showNotification(`Коллекция "${collName}" создана`, 'success');
- viewDatabase(currentDatabase);
- } else {
- const error = await response.json();
- showNotification(error.error || 'Ошибка создания коллекции', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
- };
-}
-
-/**
- * Показывает модальное окно вставки документа
- */
-function showInsertDocumentModal() {
- if (!currentDatabase || !currentCollection) {
- showNotification('Сначала выберите базу данных и коллекцию', 'warning');
- return;
- }
- DOM.modalTitle.textContent = 'Вставить документ';
- DOM.modalBody.innerHTML = `
-
-
-
- `;
- DOM.modalConfirm.textContent = 'Вставить';
- DOM.modal.classList.add('show');
-
- DOM.modalConfirm.onclick = async () => {
- const docData = document.getElementById('docData').value;
- if (!docData) {
- showNotification('Введите данные документа', 'error');
- return;
- }
- try {
- const data = JSON.parse(docData);
- const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(data)
- });
- const result = await response.json();
- if (result.success) {
- DOM.modal.classList.remove('show');
- showNotification('Документ вставлен', 'success');
- viewCollection(currentDatabase, currentCollection);
- } else {
- showNotification(result.error || 'Ошибка вставки документа', 'error');
- }
- } catch (error) {
- showNotification(error instanceof SyntaxError ? 'Неверный формат JSON' : 'Ошибка подключения', 'error');
- }
- };
-}
-
-/**
- * Показывает модальное окно обновления документа
- * @param {string} docId - ID документа
- * @param {Object} currentFields - Текущие поля документа
- */
-function showUpdateDocumentModal(docId = '', currentFields = null) {
- if (!currentDatabase || !currentCollection) {
- showNotification('Сначала выберите базу данных и коллекцию', 'warning');
- return;
- }
- DOM.modalTitle.textContent = 'Обновить документ';
- DOM.modalBody.innerHTML = `
-
-
-
-
- `;
- DOM.modalConfirm.textContent = 'Обновить';
- DOM.modal.classList.add('show');
-
- DOM.modalConfirm.onclick = async () => {
- const updateDocId = document.getElementById('updateDocId').value;
- const updateData = document.getElementById('updateData').value;
- if (!updateDocId) {
- showNotification('Введите ID документа', 'error');
- return;
- }
- if (!updateData) {
- showNotification('Введите данные для обновления', 'error');
- return;
- }
- try {
- const data = JSON.parse(updateData);
- const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}?id=${encodeURIComponent(updateDocId)}`, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(data)
- });
- const result = await response.json();
- if (result.success) {
- DOM.modal.classList.remove('show');
- showNotification('Документ обновлён', 'success');
- viewCollection(currentDatabase, currentCollection);
- } else {
- showNotification(result.error || 'Ошибка обновления документа', 'error');
- }
- } catch (error) {
- showNotification(error instanceof SyntaxError ? 'Неверный формат JSON' : 'Ошибка подключения', 'error');
- }
- };
-}
-
-// ======================== ТРАНЗАКЦИИ ========================
-
-/**
- * Загружает и отображает список активных транзакций
- */
-async function loadTransactionList() {
- DOM.title.textContent = 'Активные транзакции';
- DOM.content.innerHTML = '';
-
- try {
- const response = await fetch('/api/webui/transactions');
- const data = await response.json();
-
- if (data.success) {
- DOM.content.innerHTML = `
-
-
-
-
-
-
-
- Транзакции
-
| ID | Статус | Начало | Операций | Действия |
- ${data.data.map(tx => `
- ${escapeHtml(tx.id)} |
- ${escapeHtml(tx.status)} |
- ${new Date(tx.start_time).toLocaleString()} |
- ${tx.operation_count || 0} |
-
-
- ${tx.status === 'active' ? `` : ''}
- |
-
`).join('') || '| Нет активных транзакций |
'}
-
- `;
- } else {
- DOM.content.innerHTML = 'Ошибка загрузки транзакций
';
- }
- } catch (error) {
- DOM.content.innerHTML = 'Ошибка подключения
';
- }
-}
-
-/**
- * Загружает детали транзакции
- * @param {string} txId - ID транзакции
- */
-async function loadTransactionDetails(txId) {
- DOM.modalTitle.textContent = `Детали транзакции ${txId}`;
- DOM.modalConfirm.textContent = 'Закрыть';
- DOM.modalBody.innerHTML = '';
- DOM.modal.classList.add('show');
-
- const originalConfirmHandler = DOM.modalConfirm.onclick;
- DOM.modalConfirm.onclick = () => {
- DOM.modal.classList.remove('show');
- DOM.modalConfirm.onclick = originalConfirmHandler;
- };
-
- try {
- const response = await fetch(`/api/webui/transaction/${txId}/details`);
- const data = await response.json();
- if (data.success && data.data) {
- const tx = data.data;
- DOM.modalBody.innerHTML = `ID: ${escapeHtml(tx.id)}
- Статус: ${escapeHtml(tx.status)}
- Время начала: ${new Date(tx.start_time).toLocaleString()}
- Количество операций: ${tx.operation_count}
-
Операции
- ${tx.operations?.length ? `| Тип | БД | Коллекция | ID документа |
${tx.operations.map(op => `| ${escapeHtml(op.type)} | ${escapeHtml(op.database)} | ${escapeHtml(op.collection)} | ${escapeHtml(op.document_id)} |
`).join('')}
` : 'Нет операций
'}`;
- } else {
- DOM.modalBody.innerHTML = `Ошибка загрузки деталей
`;
- }
- } catch (error) {
- DOM.modalBody.innerHTML = 'Ошибка подключения
';
- }
-}
-
-/**
- * Начинает новую сессию транзакций
- */
-async function startSession() {
- try {
- const response = await fetch('/api/webui/transactions', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ action: 'start_session' })
- });
- const data = await response.json();
- if (data.success) {
- showNotification('Сессия начата', 'success');
- loadTransactionList();
- } else {
- showNotification(data.error || 'Ошибка', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
-}
-
-/**
- * Начинает новую транзакцию
- */
-async function startTransaction() {
- try {
- const response = await fetch('/api/webui/transactions', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ action: 'start_transaction' })
- });
- const data = await response.json();
- if (data.success) {
- showNotification('Транзакция начата', 'success');
- loadTransactionList();
- } else {
- showNotification(data.error || 'Ошибка', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
-}
-
-/**
- * Фиксирует текущую транзакцию
- */
-async function commitTransaction() {
- try {
- const response = await fetch('/api/webui/transaction/commit', { method: 'POST' });
- const data = await response.json();
- if (data.success) {
- showNotification('Транзакция зафиксирована', 'success');
- loadTransactionList();
- } else {
- showNotification(data.error || 'Ошибка', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
-}
-
-/**
- * Отменяет текущую транзакцию
- */
-async function abortTransaction() {
- try {
- const response = await fetch('/api/webui/transaction/abort', { method: 'POST' });
- const data = await response.json();
- if (data.success) {
- showNotification('Транзакция отменена', 'success');
- loadTransactionList();
- } else {
- showNotification(data.error || 'Ошибка', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
-}
-
-/**
- * Фиксирует транзакцию по ID
- * @param {string} txId - ID транзакции
- */
-async function commitTransactionById(txId) {
- if (!confirm(`Зафиксировать транзакцию ${txId}?`)) return;
- try {
- const response = await fetch(`/api/webui/transaction/${txId}/commit`, { method: 'POST' });
- const data = await response.json();
- if (data.success) {
- showNotification(`Транзакция ${txId} зафиксирована`, 'success');
- loadTransactionList();
- } else {
- showNotification(data.error || 'Ошибка фиксации', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
-}
-
-/**
- * Отменяет транзакцию по ID
- * @param {string} txId - ID транзакции
- */
-async function abortTransactionById(txId) {
- if (!confirm(`Отменить транзакцию ${txId}?`)) return;
- try {
- const response = await fetch(`/api/webui/transaction/${txId}/abort`, { method: 'POST' });
- const data = await response.json();
- if (data.success) {
- showNotification(`Транзакция ${txId} отменена`, 'success');
- loadTransactionList();
- } else {
- showNotification(data.error || 'Ошибка отмены', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
-}
-
-// ======================== ОСТАЛЬНЫЕ ФУНКЦИИ (ЗАГЛУШКИ ДЛЯ ПОЛНОТЫ) ========================
-
-// ACL функции
-async function loadACLUsers() { DOM.content.innerHTML = 'Раздел в разработке
'; }
-async function loadACLRoles() { DOM.content.innerHTML = 'Раздел в разработке
'; }
-async function loadACLPermissions() { DOM.content.innerHTML = 'Раздел в разработке
'; }
-
-// Индексы
-async function loadIndexesList() { DOM.content.innerHTML = 'Раздел в разработке
'; }
-
-// Импорт/Экспорт
-async function loadExportPage() { DOM.content.innerHTML = 'Раздел в разработке
'; }
-async function loadImportPage() { DOM.content.innerHTML = 'Раздел в разработке
'; }
-
-// Ограничения
-async function loadConstraintsList() { DOM.content.innerHTML = 'Раздел в разработке
'; }
-
-// Триггеры
-async function loadTriggersList() { DOM.content.innerHTML = 'Раздел в разработке
'; }
-async function loadTriggerLog() { DOM.content.innerHTML = 'Раздел в разработке
'; }
-
-// Модальные окна для ACL и ограничений
-function showCreateUserModal() { showNotification('Функция в разработке', 'info'); }
-function showCreateRoleModal() { showNotification('Функция в разработке', 'info'); }
-function showCreateIndexModal() { showNotification('Функция в разработке', 'info'); }
-function showConstraintModal(type) { showNotification(`Добавление ограничения "${type}" в разработке`, 'info'); }