diff --git a/internal/api/static/app.js b/internal/api/static/app.js
deleted file mode 100644
index e7cf296..0000000
--- a/internal/api/static/app.js
+++ /dev/null
@@ -1,959 +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
- */
-
-// Файл: internal/api/static/app.js
-// JavaScript для веб-интерфейса Futriis DB Dashboard
-
-// Глобальное состояние
-let currentSession = null;
-let currentDatabase = null;
-let currentCollection = null;
-let currentUser = null;
-
-// DOM элементы
-const contentArea = document.getElementById('contentArea');
-const pageTitle = document.getElementById('pageTitle');
-const connectionStatus = document.getElementById('connectionStatus');
-const userInfoSpan = document.querySelector('#userInfo span');
-const logoutBtn = document.getElementById('logoutBtn');
-const menuToggle = document.getElementById('menuToggle');
-const sidebar = document.querySelector('.sidebar');
-const modal = document.getElementById('modal');
-const modalTitle = document.getElementById('modalTitle');
-const modalBody = document.getElementById('modalBody');
-const modalConfirm = document.getElementById('modalConfirm');
-const modalCloseBtns = document.querySelectorAll('.modal-close');
-
-// Инициализация приложения
-document.addEventListener('DOMContentLoaded', () => {
- checkSession();
- initNavigation();
- initEventListeners();
-});
-
-// Проверка сессии
-async function checkSession() {
- try {
- const response = await fetch('/api/webui/session');
- const data = await response.json();
-
- if (data.success && data.data.authenticated) {
- currentUser = data.data.username;
- userInfoSpan.textContent = currentUser;
-
- if (data.data.connection_status === 'connected') {
- connectionStatus.className = 'connection-status online';
- connectionStatus.innerHTML = 'СУБД подключена';
- } else {
- connectionStatus.className = 'connection-status offline';
- connectionStatus.innerHTML = 'СУБД не подключена';
- }
-
- loadDashboard();
- } else {
- showLoginModal();
- }
- } catch (error) {
- console.error('Session check failed:', error);
- showLoginModal();
- }
-}
-
-// Функция для проверки статуса подключения
-function startConnectionStatusMonitor() {
- setInterval(async () => {
- if (currentUser) {
- try {
- const response = await fetch('/api/webui/session');
- const data = await response.json();
-
- if (data.success && data.data.connection_status === 'connected') {
- connectionStatus.className = 'connection-status online';
- connectionStatus.innerHTML = 'СУБД подключена';
- } else {
- connectionStatus.className = 'connection-status offline';
- connectionStatus.innerHTML = 'СУБД не подключена';
- }
- } catch (error) {
- connectionStatus.className = 'connection-status offline';
- connectionStatus.innerHTML = 'СУБД не подключена';
- }
- }
- }, 5000);
-}
-
-// Показать модальное окно входа
-function showLoginModal() {
- modalTitle.textContent = 'Вход в систему субд Futriis';
- modalBody.innerHTML = `
-
-
-
-
-
-
-
-
- `;
-
- // Меняем текст на кнопке "Подтвердить" на "Войти"
- modalConfirm.textContent = 'Войти';
-
- modal.classList.add('show');
-
- const confirmHandler = async () => {
- const username = document.getElementById('username').value;
- const password = document.getElementById('password').value;
-
- if (!username || !password) {
- showNotification('Пожалуйста, заполните все поля', 'error');
- return;
- }
-
- try {
- const response = await fetch('/api/webui/login', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ username, password })
- });
-
- const data = await response.json();
-
- if (data.success) {
- currentUser = username;
- userInfoSpan.textContent = username;
- modal.classList.remove('show');
- showNotification('Вход выполнен успешно', 'success');
- connectionStatus.className = 'connection-status online';
- connectionStatus.innerHTML = 'СУБД подключена';
- startConnectionStatusMonitor();
- loadDashboard();
- } else {
- showNotification(data.error || 'Неверный логин и/или пароль', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения к серверу', 'error');
- }
- };
-
- modalConfirm.onclick = confirmHandler;
-
- // Обработка Enter
- const handleEnter = (e) => {
- if (e.key === 'Enter') {
- confirmHandler();
- document.removeEventListener('keydown', handleEnter);
- }
- };
- document.addEventListener('keydown', handleEnter);
-}
-
-// Инициализация навигации
-function initNavigation() {
- document.querySelectorAll('.nav-link[data-section]').forEach(link => {
- link.addEventListener('click', (e) => {
- e.preventDefault();
- const section = link.dataset.section;
- loadSection(section);
- setActiveNav(link);
- });
- });
-
- document.querySelectorAll('[data-action]').forEach(item => {
- item.addEventListener('click', (e) => {
- e.preventDefault();
- const action = item.dataset.action;
- handleCrudAction(action);
- });
- });
-
- document.querySelectorAll('.has-submenu > .nav-link').forEach(link => {
- link.addEventListener('click', (e) => {
- e.preventDefault();
- const parent = link.closest('.has-submenu');
- parent.classList.toggle('open');
- });
- });
-}
-
-// Инициализация обработчиков событий
-function initEventListeners() {
- logoutBtn.addEventListener('click', async () => {
- await fetch('/api/webui/logout', { method: 'POST' });
- currentSession = null;
- currentUser = null;
- connectionStatus.className = 'connection-status offline';
- connectionStatus.innerHTML = 'СУБД не подключена';
- showLoginModal();
- });
-
- if (menuToggle) {
- menuToggle.addEventListener('click', () => {
- sidebar.classList.toggle('open');
- });
- }
-
- modalCloseBtns.forEach(btn => {
- btn.addEventListener('click', () => {
- modal.classList.remove('show');
- });
- });
-
- modal.addEventListener('click', (e) => {
- if (e.target === modal) {
- modal.classList.remove('show');
- }
- });
-}
-
-// Загрузка секции
-async function loadSection(section) {
- switch(section) {
- case 'dashboard':
- loadDashboard();
- break;
- case 'cluster':
- loadClusterManagement();
- break;
- case 'audit':
- loadAuditLog();
- break;
- case 'settings':
- loadSettings();
- break;
- default:
- loadDashboard();
- }
-}
-
-// Загрузка дашборда
-async function loadDashboard() {
- pageTitle.textContent = 'Панель управления';
- contentArea.innerHTML = '';
-
- try {
- const [statsRes, dbsRes] = await Promise.all([
- fetch('/api/webui/stats'),
- fetch('/api/webui/databases')
- ]);
-
- const stats = await statsRes.json();
- const databases = await dbsRes.json();
-
- contentArea.innerHTML = `
-
-
-
-
-
${stats.data.databases || 0}
-
Базы данных
-
-
-
-
-
-
${stats.data.collections || 0}
-
Коллекции
-
-
-
-
-
-
${stats.data.documents || 0}
-
Документы
-
-
-
-
-
-
${stats.data.storage_used_mb?.toFixed(2) || 0} MB
-
Использовано памяти
-
-
-
-
-
-
Базы данных
-
-
- | Имя БД | Коллекции | Действия |
-
-
- ${databases.data.map(db => `
-
- | ${escapeHtml(db.name)} |
- ${db.collections} |
-
-
- |
-
- `).join('')}
-
-
-
- `;
- } catch (error) {
- contentArea.innerHTML = 'Ошибка загрузки данных
';
- showNotification('Ошибка загрузки дашборда', 'error');
- }
-}
-
-// Просмотр базы данных
-window.viewDatabase = async function(dbName) {
- currentDatabase = dbName;
- pageTitle.textContent = `База данных: ${dbName}`;
- contentArea.innerHTML = '';
-
- try {
- const response = await fetch(`/api/webui/collections/${dbName}`);
- const data = await response.json();
-
- if (data.success) {
- contentArea.innerHTML = `
-
-
-
Коллекции
-
-
-
-
- | Имя коллекции | Документов | Размер | Индексы | Действия |
-
-
- ${data.data.collections.map(coll => `
-
- | ${escapeHtml(coll.name)} |
- ${coll.count} |
- ${(coll.size / 1024).toFixed(2)} KB |
- ${coll.indexes.length} |
-
-
-
- |
-
- `).join('')}
-
-
-
- `;
- } else {
- contentArea.innerHTML = 'Ошибка загрузки коллекций
';
- }
- } catch (error) {
- contentArea.innerHTML = 'Ошибка подключения
';
- }
-};
-
-// Просмотр коллекции
-window.viewCollection = async function(dbName, collName) {
- currentDatabase = dbName;
- currentCollection = collName;
- pageTitle.textContent = `Коллекция: ${dbName}.${collName}`;
- contentArea.innerHTML = '';
-
- try {
- const response = await fetch(`/api/webui/documents/${dbName}/${collName}?limit=100`);
- const data = await response.json();
-
- if (data.success) {
- contentArea.innerHTML = `
-
-
-
-
-
-
-
Документы (${data.data.total} всего)
-
-
- | ID | Поля | Создан | Действия |
-
-
- ${data.data.documents.map(doc => `
-
- ${escapeHtml(doc.id)} |
- ${escapeHtml(JSON.stringify(doc.fields, null, 2))} |
- ${new Date(doc.created_at).toLocaleString()} |
-
-
-
- |
-
- `).join('')}
-
-
-
- `;
- } else {
- contentArea.innerHTML = 'Ошибка загрузки документов
';
- }
- } catch (error) {
- contentArea.innerHTML = 'Ошибка подключения
';
- }
-};
-
-// Загрузка управления кластером
-async function loadClusterManagement() {
- pageTitle.textContent = 'Управление кластером';
- contentArea.innerHTML = 'Загрузка информации о кластере...
';
-
- try {
- const [statusRes, nodesRes] = await Promise.all([
- fetch('/api/webui/cluster/status'),
- fetch('/api/webui/cluster/nodes')
- ]);
-
- const status = await statusRes.json();
- const nodes = await nodesRes.json();
-
- contentArea.innerHTML = `
-
-
-
-
-
- ${status.data.health === 'healthy' ? 'Здоров' : status.data.health === 'degraded' ? 'Деградирован' : 'Критический'}
-
-
Состояние кластера
-
-
-
-
-
-
${status.data.active_nodes}/${status.data.total_nodes}
-
Активные узлы
-
-
-
-
-
-
${status.data.replication_factor}
-
Фактор репликации
-
-
-
-
-
-
Узлы кластера
-
-
- | ID узла | Адрес | Статус | Последний контакт |
-
-
- ${nodes.data.map(node => `
-
- ${escapeHtml(node.id)} |
- ${escapeHtml(node.ip)}:${node.port} |
- ${node.status} |
- ${new Date(node.last_seen * 1000).toLocaleString()} |
-
- `).join('')}
-
-
-
- `;
- } catch (error) {
- contentArea.innerHTML = 'Ошибка загрузки информации о кластере
';
- }
-}
-
-// Загрузка лога аудита
-async function loadAuditLog() {
- pageTitle.textContent = 'Лог аудита';
- contentArea.innerHTML = '';
- contentArea.innerHTML = 'Функция в разработке
';
-}
-
-// Загрузка настроек
-function loadSettings() {
- pageTitle.textContent = 'Настройки';
- contentArea.innerHTML = `
-
-
Настройки интерфейса
-
-
-
-
-
-
- `;
-}
-
-// Обработка CRUD действий
-function handleCrudAction(action) {
- switch(action) {
- case 'create-db':
- showCreateDatabaseModal();
- break;
- case 'create-collection':
- showCreateCollectionModal();
- break;
- case 'insert-doc':
- showInsertDocumentModal();
- break;
- case 'find-doc':
- showFindDocumentModal();
- break;
- case 'update-doc':
- showUpdateDocumentModal();
- break;
- case 'delete-doc':
- showDeleteDocumentModal();
- break;
- }
-}
-
-// Показать модальное окно создания БД
-function showCreateDatabaseModal() {
- modalTitle.textContent = 'Создать базу данных';
- modalConfirm.textContent = 'Подтвердить';
- modalBody.innerHTML = `
-
-
-
-
- `;
-
- modal.classList.add('show');
-
- modalConfirm.onclick = async () => {
- const dbName = document.getElementById('dbName').value;
- if (!dbName) {
- showNotification('Введите имя базы данных', 'error');
- return;
- }
-
- try {
- const response = await fetch('/api/db/' + dbName, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({})
- });
-
- if (response.ok) {
- modal.classList.remove('show');
- showNotification(`База данных "${dbName}" создана`, 'success');
- loadDashboard();
- } else {
- const error = await response.json();
- showNotification(error.error || 'Ошибка создания БД', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
- };
-}
-
-// Показать модальное окно создания коллекции
-function showCreateCollectionModal() {
- if (!currentDatabase) {
- showNotification('Сначала выберите базу данных', 'warning');
- return;
- }
-
- modalTitle.textContent = 'Создать коллекцию';
- modalConfirm.textContent = 'Подтвердить';
- modalBody.innerHTML = `
-
-
-
-
-
-
-
-
- `;
-
- modal.classList.add('show');
-
- modalConfirm.onclick = async () => {
- const collName = document.getElementById('collName').value;
- if (!collName) {
- showNotification('Введите имя коллекции', 'error');
- return;
- }
-
- try {
- const response = await fetch(`/api/db/${currentDatabase}/${collName}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({})
- });
-
- if (response.ok) {
- modal.classList.remove('show');
- showNotification(`Коллекция "${collName}" создана`, 'success');
- viewDatabase(currentDatabase);
- } else {
- const error = await response.json();
- showNotification(error.error || 'Ошибка создания коллекции', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
- };
-}
-
-// Показать модальное окно вставки документа
-function showInsertDocumentModal() {
- if (!currentDatabase || !currentCollection) {
- showNotification('Сначала выберите базу данных и коллекцию', 'warning');
- return;
- }
-
- modalTitle.textContent = 'Вставить документ';
- modalConfirm.textContent = 'Подтвердить';
- modalBody.innerHTML = `
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- modal.classList.add('show');
-
- modalConfirm.onclick = async () => {
- const docData = document.getElementById('docData').value;
- if (!docData) {
- showNotification('Введите данные документа', 'error');
- return;
- }
-
- try {
- const data = JSON.parse(docData);
- const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(data)
- });
-
- const result = await response.json();
-
- if (result.success) {
- modal.classList.remove('show');
- showNotification('Документ вставлен', 'success');
- viewCollection(currentDatabase, currentCollection);
- } else {
- showNotification(result.error || 'Ошибка вставки документа', 'error');
- }
- } catch (error) {
- if (error instanceof SyntaxError) {
- showNotification('Неверный формат JSON', 'error');
- } else {
- showNotification('Ошибка подключения', 'error');
- }
- }
- };
-}
-
-// Показать модальное окно поиска документа
-function showFindDocumentModal() {
- if (!currentDatabase || !currentCollection) {
- showNotification('Сначала выберите базу данных и коллекцию', 'warning');
- return;
- }
-
- modalTitle.textContent = 'Найти документ';
- modalConfirm.textContent = 'Найти';
- modalBody.innerHTML = `
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- modal.classList.add('show');
-
- modalConfirm.onclick = async () => {
- const docId = document.getElementById('docId').value;
- if (!docId) {
- showNotification('Введите ID документа', 'error');
- return;
- }
-
- try {
- const response = await fetch(`/api/db/${currentDatabase}/${currentCollection}/${docId}`);
-
- if (response.ok) {
- const data = await response.json();
- modal.classList.remove('show');
-
- contentArea.innerHTML = `
-
-
Результат поиска
-
- ${escapeHtml(JSON.stringify(data.data, null, 2))}
-
-
-
- `;
- } else {
- const error = await response.json();
- showNotification(error.error || 'Документ не найден', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
- };
-}
-
-// Показать модальное окно обновления документа
-function showUpdateDocumentModal(docId, currentFields = null) {
- if (!currentDatabase || !currentCollection) {
- showNotification('Сначала выберите базу данных и коллекцию', 'warning');
- return;
- }
-
- const fieldsJson = currentFields ? JSON.stringify(currentFields, null, 2) : '';
-
- modalTitle.textContent = 'Обновить документ';
- modalConfirm.textContent = 'Обновить';
- modalBody.innerHTML = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- modal.classList.add('show');
-
- modalConfirm.onclick = async () => {
- const updateDocId = document.getElementById('updateDocId').value;
- const updateData = document.getElementById('updateData').value;
-
- if (!updateDocId) {
- showNotification('Введите ID документа', 'error');
- return;
- }
- if (!updateData) {
- showNotification('Введите данные для обновления', 'error');
- return;
- }
-
- try {
- const data = JSON.parse(updateData);
- const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}?id=${encodeURIComponent(updateDocId)}`, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(data)
- });
-
- const result = await response.json();
-
- if (result.success) {
- modal.classList.remove('show');
- showNotification('Документ обновлён', 'success');
- viewCollection(currentDatabase, currentCollection);
- } else {
- showNotification(result.error || 'Ошибка обновления документа', 'error');
- }
- } catch (error) {
- if (error instanceof SyntaxError) {
- showNotification('Неверный формат JSON', 'error');
- } else {
- showNotification('Ошибка подключения', 'error');
- }
- }
- };
-}
-
-// Показать модальное окно удаления документа
-function showDeleteDocumentModal() {
- if (!currentDatabase || !currentCollection) {
- showNotification('Сначала выберите базу данных и коллекцию', 'warning');
- return;
- }
-
- modalTitle.textContent = 'Удалить документ';
- modalConfirm.textContent = 'Удалить';
- modalBody.innerHTML = `
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- modal.classList.add('show');
-
- modalConfirm.onclick = async () => {
- const docId = document.getElementById('deleteDocId').value;
- if (!docId) {
- showNotification('Введите ID документа', 'error');
- return;
- }
-
- try {
- const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}?id=${encodeURIComponent(docId)}`, {
- method: 'DELETE'
- });
-
- const result = await response.json();
-
- if (result.success) {
- modal.classList.remove('show');
- showNotification('Документ удалён', 'success');
- viewCollection(currentDatabase, currentCollection);
- } else {
- showNotification(result.error || 'Ошибка удаления документа', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
- };
-}
-
-// Удаление коллекции
-window.deleteCollection = async function(dbName, collName) {
- if (confirm(`Вы уверены, что хотите удалить коллекцию "${collName}"? Это действие необратимо.`)) {
- try {
- const response = await fetch(`/api/db/${dbName}/${collName}`, {
- method: 'DELETE'
- });
-
- if (response.ok) {
- showNotification(`Коллекция "${collName}" удалена`, 'success');
- viewDatabase(dbName);
- } else {
- const error = await response.json();
- showNotification(error.error || 'Ошибка удаления коллекции', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
- }
-};
-
-// Удаление документа
-window.deleteDocument = async function(dbName, collName, docId) {
- if (confirm(`Вы уверены, что хотите удалить документ "${docId}"?`)) {
- try {
- const response = await fetch(`/api/webui/documents/${dbName}/${collName}?id=${encodeURIComponent(docId)}`, {
- method: 'DELETE'
- });
-
- const result = await response.json();
-
- if (result.success) {
- showNotification('Документ удалён', 'success');
- viewCollection(dbName, collName);
- } else {
- showNotification(result.error || 'Ошибка удаления документа', 'error');
- }
- } catch (error) {
- showNotification('Ошибка подключения', 'error');
- }
- }
-};
-
-// Сохранение настроек
-function saveSettings() {
- const theme = document.getElementById('themeSelect')?.value;
- if (theme) {
- localStorage.setItem('theme', theme);
- showNotification('Настройки сохранены', 'success');
- }
-}
-
-// Утилиты
-function escapeHtml(str) {
- if (!str) return '';
- return String(str)
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
-}
-
-function showNotification(message, type = 'info') {
- const container = document.getElementById('notificationContainer');
- const notification = document.createElement('div');
- notification.className = `notification ${type}`;
-
- let icon = '';
- switch(type) {
- case 'success': icon = ''; break;
- case 'error': icon = ''; break;
- case 'warning': icon = ''; break;
- default: icon = '';
- }
-
- notification.innerHTML = `${icon}${escapeHtml(message)}`;
- container.appendChild(notification);
-
- setTimeout(() => {
- notification.style.animation = 'slideOutRight 0.3s ease';
- setTimeout(() => notification.remove(), 300);
- }, 3000);
-}
-
-function setActiveNav(activeLink) {
- document.querySelectorAll('.nav-link').forEach(link => {
- link.classList.remove('active');
- });
- activeLink.classList.add('active');
-}