/* * 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; connectionStatus.classList.add('online'); connectionStatus.classList.remove('offline'); loadDashboard(); } else { showLoginModal(); } } catch (error) { console.error('Session check failed:', error); showLoginModal(); } } // Показать модальное окно входа function showLoginModal() { modalTitle.textContent = 'Вход в систему'; modalBody.innerHTML = `
`; 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'); 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); }); }); // Обработка подменю CRUD 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; 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 => ` `).join('')}
Имя БДКоллекцииДействия
${escapeHtml(db.name)} ${db.collections}
`; } catch (error) { contentArea.innerHTML = '
Ошибка загрузки данных
'; showNotification('Ошибка загрузки дашборда', 'error'); } } // Просмотр базы данных window.viewDatabase = async function(dbName) { currentDatabase = dbName; pageTitle.textContent = `База данных: ${dbName}`; contentArea.innerHTML = '

Загрузка коллекций...

'; try { const response = await fetch(`/api/webui/collections/${dbName}`); const data = await response.json(); if (data.success) { contentArea.innerHTML = `

Коллекции

${data.data.collections.map(coll => ` `).join('')}
Имя коллекцииДокументовРазмерИндексыДействия
${escapeHtml(coll.name)} ${coll.count} ${(coll.size / 1024).toFixed(2)} KB ${coll.indexes.length}
`; } else { contentArea.innerHTML = '
Ошибка загрузки коллекций
'; } } catch (error) { contentArea.innerHTML = '
Ошибка подключения
'; } }; // Просмотр коллекции window.viewCollection = async function(dbName, collName) { currentDatabase = dbName; currentCollection = collName; pageTitle.textContent = `Коллекция: ${dbName}.${collName}`; contentArea.innerHTML = '

Загрузка документов...

'; try { const response = await fetch(`/api/webui/documents/${dbName}/${collName}?limit=100`); const data = await response.json(); if (data.success) { contentArea.innerHTML = `

Документы (${data.data.total} всего)

${data.data.documents.map(doc => ` `).join('')}
IDПоляСозданДействия
${escapeHtml(doc.id)}
${escapeHtml(JSON.stringify(doc.fields, null, 2))}
${new Date(doc.created_at).toLocaleString()}
`; } else { contentArea.innerHTML = '
Ошибка загрузки документов
'; } } catch (error) { contentArea.innerHTML = '
Ошибка подключения
'; } }; // Загрузка управления кластером async function loadClusterManagement() { pageTitle.textContent = 'Управление кластером'; contentArea.innerHTML = '

Загрузка информации о кластере...

'; try { const [statusRes, nodesRes] = await Promise.all([ fetch('/api/webui/cluster/status'), fetch('/api/webui/cluster/nodes') ]); const status = await statusRes.json(); const nodes = await nodesRes.json(); contentArea.innerHTML = `

${status.data.health === 'healthy' ? 'Здоров' : status.data.health === 'degraded' ? 'Деградирован' : 'Критический'}

Состояние кластера

${status.data.active_nodes}/${status.data.total_nodes}

Активные узлы

${status.data.replication_factor}

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

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

${nodes.data.map(node => ` `).join('')}
ID узлаАдресСтатусПоследний контакт
${escapeHtml(node.id)} ${escapeHtml(node.ip)}:${node.port} ${node.status} ${new Date(node.last_seen * 1000).toLocaleString()}
`; } catch (error) { contentArea.innerHTML = '
Ошибка загрузки информации о кластере
'; } } // Загрузка лога аудита async function loadAuditLog() { pageTitle.textContent = 'Лог аудита'; contentArea.innerHTML = '

Загрузка лога аудита...

'; // TODO: Реализовать API для получения лога аудита 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 = 'Создать базу данных'; 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 = 'Создать коллекцию'; 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 = 'Вставить документ'; 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 = 'Найти документ'; 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 = 'Обновить документ'; 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 = 'Удалить документ'; 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 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'); }