document.addEventListener('DOMContentLoaded', () => { const statusElement = document.getElementById('server-status'); const refreshBtn = document.getElementById('refresh-status-btn'); const logsElement = document.getElementById('logs-content'); const configForm = document.getElementById('config-form'); const payslipsPathInput = document.getElementById('payslipsPath'); const configStatus = document.getElementById('config-status'); const browseBtn = document.getElementById('browse-btn'); const dbStatusElement = document.getElementById('db-status'); const checkDbBtn = document.getElementById('check-db-btn'); const refreshLogsBtn = document.getElementById('refresh-logs-btn'); const clearLogsBtn = document.getElementById('clear-logs-btn'); const adminDbStatusEl = document.getElementById('admin-db-status'); const adminDbMessageEl = document.getElementById('admin-db-message'); const backendDbStatusEl = document.getElementById('backend-db-status'); const backendDbMessageEl = document.getElementById('backend-db-message'); const pingBtn = document.getElementById('ping-btn'); const pingStopBtn = document.getElementById('ping-stop-btn'); const pingClearBtn = document.getElementById('ping-clear-btn'); const pingIpInput = document.getElementById('ping-ip'); const pingOutputEl = document.getElementById('ping-output'); let ws; const loginForm = document.getElementById('login-form'); if (loginForm) { loginForm.addEventListener('submit', async (e) => { e.preventDefault(); const password = loginForm.elements['password'].value; try { const response = await fetch('/api/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) }); if (response.ok) { window.location.href = '/'; } else { alert('Неверный пароль'); } } catch (err) { alert('Ошибка авторизации'); } }); } function fetchStatus() { fetch('/api/status') .then(response => response.json()) .then(data => { if (data.status === 'online') { statusElement.textContent = 'Онлайн'; statusElement.style.color = 'green'; } else { statusElement.textContent = 'Оффлайн'; statusElement.style.color = 'red'; } }) .catch(err => { console.error('Ошибка при получении статуса:', err); statusElement.textContent = 'Ошибка'; statusElement.style.color = 'orange'; }); } refreshBtn.addEventListener('click', fetchStatus); function fetchLogs() { logsElement.textContent = 'загрузка...'; fetch('/api/logs') .then(response => { if (!response.ok) throw new Error('Не удалось загрузить логи'); return response.text(); }) .then(data => { logsElement.textContent = data || 'Лог-файл пуст.'; }) .catch(err => { logsElement.textContent = `Ошибка при загрузке логов: ${err.message}`; logsElement.style.color = '#ff6b6b'; }); } function fetchConfig() { fetch('/api/config') .then(response => response.json()) .then(config => { payslipsPathInput.value = config.payslipsPath; }) .catch(err => { console.error('Ошибка при получении конфигурации:', err); configStatus.textContent = 'Не удалось загрузить конфигурацию.'; configStatus.style.color = 'red'; }); } configForm.addEventListener('submit', (e) => { e.preventDefault(); const newConfig = { payslipsPath: payslipsPathInput.value }; apiFetch('/api/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newConfig) }) .then(response => response.json()) .then(data => { configStatus.textContent = 'Конфигурация успешно сохранена!'; configStatus.style.color = 'green'; setTimeout(() => { configStatus.textContent = ''; }, 3000); }) .catch(err => { console.error('Ошибка при сохранении конфигурации:', err); configStatus.textContent = 'Ошибка при сохранении.'; configStatus.style.color = 'red'; }); }); browseBtn.addEventListener('click', async () => { try { const response = await fetch('/api/browse'); if (response.ok) { const data = await response.json(); payslipsPathInput.value = data.path; } else { console.info('Folder selection was canceled.'); } } catch (error) { console.error('Error opening folder dialog:', error); alert('Не удалось открыть диалог выбора папки.'); } }); function renderStatus(element, messageEl, data) { messageEl.textContent = data.message; if (data.status === 'online') { element.textContent = 'Онлайн'; element.style.color = 'green'; } else { element.textContent = 'Оффлайн'; element.style.color = 'red'; } } function checkDbStatus() { adminDbStatusEl.textContent = 'проверка...'; backendDbStatusEl.textContent = 'проверка...'; adminDbMessageEl.textContent = ''; backendDbMessageEl.textContent = ''; adminDbStatusEl.style.color = '#333'; backendDbStatusEl.style.color = '#333'; fetch('/api/health/check-db') .then(response => response.json()) .then(data => { renderStatus(adminDbStatusEl, adminDbMessageEl, data.adminToDb); renderStatus(backendDbStatusEl, backendDbMessageEl, data.backendToDb); }) .catch(err => { console.error('Ошибка при проверке статуса БД:', err); adminDbStatusEl.textContent = 'Ошибка'; adminDbStatusEl.style.color = 'orange'; backendDbStatusEl.textContent = 'Ошибка'; backendDbStatusEl.style.color = 'orange'; }); } checkDbBtn.addEventListener('click', checkDbStatus); function clearLogs() { if (!confirm('Это действие удалит лог-файл полностью и навсегда. Вы уверены?')) { return; } fetch('/api/logs/clear', { method: 'POST' }) .then(response => { if (response.ok) { fetchLogs(); } else { alert('Не удалось очистить логи.'); } }) .catch(err => { console.error('Ошибка при очистке логов:', err); alert('Произошла ошибка при очистке логов.'); }); } refreshLogsBtn.addEventListener('click', fetchLogs); clearLogsBtn.addEventListener('click', clearLogs); function doPing() { const ip = pingIpInput.value.trim(); if (!ip) { pingOutputEl.textContent = 'Пожалуйста, введите IP-адрес.'; return; } if (ws) { ws.close(); } const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; ws = new WebSocket(`${wsProtocol}://${window.location.host}`); ws.onopen = () => { pingOutputEl.textContent = ''; pingBtn.disabled = true; pingStopBtn.disabled = false; ws.send(JSON.stringify({ type: 'ping', ip })); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); pingOutputEl.textContent += message.data; pingOutputEl.scrollTop = pingOutputEl.scrollHeight; }; ws.onerror = (error) => { console.error('WebSocket Error:', error); pingOutputEl.textContent += '\nОшибка WebSocket соединения.'; }; ws.onclose = () => { console.info('WebSocket connection closed'); pingBtn.disabled = false; pingStopBtn.disabled = true; ws = null; }; } function stopPing() { if (ws) { ws.close(); } } function clearPingOutput() { pingOutputEl.textContent = ''; } pingBtn.addEventListener('click', doPing); pingStopBtn.addEventListener('click', stopPing); pingClearBtn.addEventListener('click', clearPingOutput); const cardLogsElement = document.getElementById('card-logs-content'); const refreshCardLogsBtn = document.getElementById('refresh-card-logs-btn'); function fetchCardLogs() { cardLogsElement.textContent = 'загрузка...'; fetch('/api/card/card-logs') .then(response => { if (!response.ok) throw new Error('Не удалось загрузить логи считывателя'); return response.text(); }) .then(data => { cardLogsElement.textContent = data || 'Лог-файл пуст.'; }) .catch(err => { cardLogsElement.textContent = `Ошибка при загрузке логов: ${err.message}`; cardLogsElement.style.color = '#ff6b6b'; }); } refreshCardLogsBtn.addEventListener('click', fetchCardLogs); // --- ТАБЫ --- const tabMainBtn = document.getElementById('tab-main'); const tabSettingsBtn = document.getElementById('tab-settings'); const tabMonitoringBtn = document.getElementById('tab-monitoring'); const mainTab = document.getElementById('main-tab'); const settingsTab = document.getElementById('settings-tab'); const monitoringTab = document.getElementById('monitoring-tab'); function showTab(tab) { mainTab.style.display = 'none'; settingsTab.style.display = 'none'; monitoringTab.style.display = 'none'; tabMainBtn.classList.remove('active'); tabSettingsBtn.classList.remove('active'); tabMonitoringBtn.classList.remove('active'); if (tab === 'main') { mainTab.style.display = ''; tabMainBtn.classList.add('active'); } else if (tab === 'settings') { settingsTab.style.display = ''; tabSettingsBtn.classList.add('active'); initSettingsTab(); } else if (tab === 'monitoring') { monitoringTab.style.display = ''; tabMonitoringBtn.classList.add('active'); initMonitoringTab(); } } tabMainBtn.addEventListener('click', () => showTab('main')); tabSettingsBtn.addEventListener('click', () => showTab('settings')); tabMonitoringBtn.addEventListener('click', () => showTab('monitoring')); // --- TOAST --- function showToast(message, type = 'success') { const container = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = 'toast toast-' + type; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => container.removeChild(toast), 300); }, 3000); } // --- CSRF TOKEN --- let csrfToken = null; async function fetchCsrfToken() { try { const res = await fetch('/moders/csrf-token', { credentials: 'include' }); if (res.ok) { const data = await res.json(); csrfToken = data.csrfToken; } } catch (e) { csrfToken = null; } } // Обёртка для fetch с CSRF и credentials async function apiFetch(url, options = {}) { if (!options.headers) options.headers = {}; if (['POST','PUT','DELETE'].includes((options.method||'GET').toUpperCase())) { if (!csrfToken) await fetchCsrfToken(); options.headers['X-CSRF-Token'] = csrfToken; } options.credentials = 'include'; return fetch(url, options); } // --- Модальное окно подтверждения --- const confirmModal = document.getElementById('confirm-modal'); const confirmModalText = document.getElementById('confirm-modal-text'); const confirmModalYes = document.getElementById('confirm-modal-yes'); const confirmModalNo = document.getElementById('confirm-modal-no'); function showConfirm(text, onConfirm) { confirmModalText.textContent = text; confirmModal.style.display = 'flex'; const yesListener = () => { onConfirm(); closeConfirm(); }; const closeConfirm = () => { confirmModal.style.display = 'none'; confirmModalYes.removeEventListener('click', yesListener); confirmModalNo.removeEventListener('click', closeConfirm); } confirmModalYes.addEventListener('click', yesListener); confirmModalNo.addEventListener('click', closeConfirm); } // --- ВКЛАДКА МОНИТОРИНГА --- let monitoringInitialized = false; let allMonitoringEvents = []; // Кэш для всех событий async function initMonitoringTab() { if (monitoringInitialized) return; monitoringInitialized = true; const tableBody = document.querySelector('#monitoring-log-table tbody'); const levelFilter = document.getElementById('log-level-filter'); const sourceFilter = document.getElementById('log-source-filter'); const refreshBtn = document.getElementById('refresh-monitoring-btn'); async function loadMonitoringLogs() { tableBody.innerHTML = 'Загрузка...'; try { const res = await apiFetch('/api/monitoring/logs'); if (!res.ok) throw new Error('Failed to fetch logs'); allMonitoringEvents = await res.json(); renderTable(); } catch (e) { tableBody.innerHTML = 'Ошибка загрузки логов.'; console.error('Error loading monitoring logs:', e); } } function renderTable() { const level = levelFilter.value; const source = sourceFilter.value; const filteredEvents = allMonitoringEvents.filter(event => { const levelMatch = (level === 'all') || (event.level === level); const sourceMatch = (source === 'all') || (event.source === source); return levelMatch && sourceMatch; }); tableBody.innerHTML = ''; if (filteredEvents.length === 0) { tableBody.innerHTML = 'Нет событий для отображения.'; return; } filteredEvents.forEach(event => { const tr = document.createElement('tr'); tr.className = `log-level-${event.level.toLowerCase()}`; tr.innerHTML = ` ${new Date(event.timestamp).toLocaleString()} ${event.level} ${event.source} ${event.message} `; tableBody.appendChild(tr); }); } levelFilter.addEventListener('change', renderTable); sourceFilter.addEventListener('change', renderTable); refreshBtn.addEventListener('click', loadMonitoringLogs); loadMonitoringLogs(); } // --- НАСТРОЙКИ: инициализация вкладки --- let settingsInitialized = false; async function initSettingsTab() { if(settingsInitialized) return; settingsInitialized = true; initServerParamsSection(); initDbSection(); initCorsSection(); initModersSection(); settingsTab.addEventListener('click', (e) => { if (e.target.dataset.action === 'edit-pass') { const targetId = e.target.dataset.target; const input = document.getElementById(targetId); input.readOnly = false; input.value = ''; input.placeholder = 'Введите новый пароль...'; input.focus(); e.target.remove(); } }); } async function initServerParamsSection() { // Загрузка текущих значений try { const results = await Promise.allSettled([ apiFetch('/api/settings/admin-server'), apiFetch('/api/settings/backend-server') ]); const adminResult = results[0]; if (adminResult.status === 'fulfilled') { const adminData = await adminResult.value.json(); document.getElementById('admin-port').value = adminData.PORT || ''; document.getElementById('admin-url').value = adminData.ADMIN_URL || ''; } else { console.error('Failed to load admin-server settings:', adminResult.reason); } const backendResult = results[1]; if (backendResult.status === 'fulfilled') { const backendData = await backendResult.value.json(); document.getElementById('backend-host').value = backendData.host || ''; document.getElementById('backend-port').value = backendData.port || ''; } else { console.error('Failed to load backend-server settings:', backendResult.reason); document.getElementById('server-params-status').textContent += ' Ошибка загрузки параметров Backend.'; } } catch (e) { document.getElementById('server-params-status').textContent = 'Критическая ошибка загрузки параметров'; document.getElementById('server-params-status').style.color = 'red'; } // Обработка сохранения document.getElementById('server-params-form').addEventListener('submit', async (e) => { e.preventDefault(); const PORT = document.getElementById('admin-port').value; const ADMIN_URL = document.getElementById('admin-url').value; const host = document.getElementById('backend-host').value; const port = document.getElementById('backend-port').value; const statusEl = document.getElementById('server-params-status'); statusEl.textContent = ''; try { const [adminRes, backendRes] = await Promise.all([ apiFetch('/api/settings/admin-server', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ PORT, ADMIN_URL }) }), apiFetch('/api/settings/backend-server', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ host, port }) }) ]); if (adminRes.ok && backendRes.ok) { statusEl.innerHTML = 'Параметры успешно сохранены.
Требуется перезапуск обоих серверов для применения!'; statusEl.style.color = 'green'; showToast('Параметры успешно сохранены', 'success'); } else { statusEl.textContent = 'Ошибка сохранения'; statusEl.style.color = 'red'; showToast('Ошибка сохранения', 'error'); } } catch (e) { statusEl.textContent = 'Ошибка сохранения'; statusEl.style.color = 'red'; showToast('Ошибка сохранения', 'error'); } }); } function initDbSection() { async function loadMysql() { try { const res = await apiFetch('/api/settings/mysql'); const data = await res.json(); document.getElementById('mysql-host').value = data.MYSQL_HOST || ''; document.getElementById('mysql-port').value = data.MYSQL_PORT || ''; document.getElementById('mysql-user').value = data.MYSQL_USER || ''; const passWrapper = document.getElementById('mysql-pass').closest('.password-wrapper'); passWrapper.innerHTML = ` `; if (data.MYSQL_PASSWORD) { passWrapper.querySelector('input').placeholder = '********'; } document.getElementById('mysql-db').value = data.MYSQL_DATABASE || ''; } catch (e) { document.getElementById('mysql-status').textContent = 'Ошибка загрузки'; document.getElementById('mysql-status').style.color = 'red'; } } loadMysql(); document.getElementById('mysql-form').addEventListener('submit', async (e) => { e.preventDefault(); const payload = { MYSQL_HOST: document.getElementById('mysql-host').value, MYSQL_PORT: document.getElementById('mysql-port').value, MYSQL_USER: document.getElementById('mysql-user').value, MYSQL_DATABASE: document.getElementById('mysql-db').value }; const passInput = document.getElementById('mysql-pass'); if (!passInput.readOnly) { payload.MYSQL_PASSWORD = passInput.value; } const statusEl = document.getElementById('mysql-status'); statusEl.textContent = ''; try { const res = await apiFetch('/api/settings/mysql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (res.ok) { statusEl.innerHTML = 'Сохранено.
Требуется перезапуск Admin-server для применения!'; statusEl.style.color = 'green'; showToast('Сохранено', 'success'); loadMysql(); // Перезагружаем секцию, чтобы сбросить поле пароля } else { statusEl.textContent = 'Ошибка сохранения'; statusEl.style.color = 'red'; showToast('Ошибка сохранения', 'error'); } } catch (e) { statusEl.textContent = 'Ошибка сохранения'; statusEl.style.color = 'red'; showToast('Ошибка сохранения', 'error'); } }); } function initCorsSection() { // --- CORS --- let allowedOrigins = []; async function loadCors() { const list = document.getElementById('cors-list'); list.innerHTML = '
  • Загрузка...
  • '; try { const res = await apiFetch('/api/settings/cors'); const data = await res.json(); allowedOrigins = Array.isArray(data.ALLOWED_ORIGINS) ? data.ALLOWED_ORIGINS : []; renderCorsList(); } catch (e) { list.innerHTML = '
  • Ошибка загрузки
  • '; } } const corsInput = document.getElementById('cors-new-origin'); const corsAddBtn = document.getElementById('cors-add-btn'); corsInput.addEventListener('input', () => { const val = corsInput.value.trim(); corsAddBtn.disabled = !val || allowedOrigins.includes(val); }); function renderCorsList() { const list = document.getElementById('cors-list'); list.innerHTML = ''; if (!allowedOrigins.length) { list.innerHTML = '
  • Нет origin
  • '; return; } allowedOrigins.forEach((origin, idx) => { const li = document.createElement('li'); li.textContent = origin + ' '; const delBtn = document.createElement('button'); delBtn.textContent = 'Удалить'; delBtn.className = 'btn-small'; delBtn.addEventListener('click', () => { allowedOrigins.splice(idx, 1); renderCorsList(); corsAddBtn.disabled = !corsInput.value.trim() || allowedOrigins.includes(corsInput.value.trim()); }); li.appendChild(delBtn); list.appendChild(li); }); corsAddBtn.disabled = !corsInput.value.trim() || allowedOrigins.includes(corsInput.value.trim()); } loadCors(); document.getElementById('cors-add-btn').addEventListener('click', () => { const input = document.getElementById('cors-new-origin'); const val = input.value.trim(); if (val && !allowedOrigins.includes(val)) { allowedOrigins.push(val); input.value = ''; renderCorsList(); corsAddBtn.disabled = true; } }); document.getElementById('cors-form').addEventListener('submit', async (e) => { e.preventDefault(); const statusEl = document.getElementById('cors-status'); statusEl.textContent = ''; try { const res = await apiFetch('/api/settings/cors', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ALLOWED_ORIGINS: allowedOrigins }) }); if (res.ok) { statusEl.innerHTML = 'Сохранено.
    Требуется перезапуск Admin-server для применения!'; statusEl.style.color = 'green'; showToast('Сохранено', 'success'); } else { statusEl.textContent = 'Ошибка сохранения'; statusEl.style.color = 'red'; showToast('Ошибка сохранения', 'error'); } } catch (e) { statusEl.textContent = 'Ошибка сохранения'; statusEl.style.color = 'red'; showToast('Ошибка сохранения', 'error'); } }); } function initModersSection() { // --- МОДЕРАТОРЫ --- async function loadModers() { const tableBody = document.querySelector('#moders-table tbody'); tableBody.innerHTML = 'Загрузка...'; try { const res = await apiFetch('/api/moders'); const moders = await res.json(); if (!Array.isArray(moders) || !moders.length) { tableBody.innerHTML = 'Нет модераторов'; return; } tableBody.innerHTML = ''; for (const moder of moders) { const tr = document.createElement('tr'); tr.dataset.id = moder.id; tr.innerHTML = `${moder.id}${moder.username}` + `` + ` ` + `` + ``; tableBody.appendChild(tr); } } catch (e) { tableBody.innerHTML = 'Ошибка загрузки'; } } loadModers(); // Добавление модератора document.getElementById('add-moder-form').addEventListener('submit', async (e) => { e.preventDefault(); const username = document.getElementById('add-moder-username').value.trim(); const password = document.getElementById('add-moder-password').value; const statusEl = document.getElementById('add-moder-status'); statusEl.textContent = ''; if (!username || !password) { statusEl.textContent = 'Заполните все поля'; statusEl.style.color = 'red'; return; } try { const res = await apiFetch('/api/moders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); if (res.ok) { statusEl.textContent = 'Модератор добавлен'; statusEl.style.color = 'green'; document.getElementById('add-moder-form').reset(); loadModers(); showToast('Модератор добавлен', 'success'); } else { const data = await res.json(); statusEl.textContent = data.message || 'Ошибка'; statusEl.style.color = 'red'; showToast(data.message || 'Ошибка', 'error'); } } catch (e) { statusEl.textContent = 'Ошибка'; statusEl.style.color = 'red'; showToast('Ошибка', 'error'); } }); // Действия: удалить/сменить пароль document.getElementById('moders-table').addEventListener('click', async (e) => { const btn = e.target.closest('button'); if (!btn) return; const id = btn.closest('tr').dataset.id; const action = btn.getAttribute('data-action'); if (action === 'delete') { showConfirm('Удалить модератора?', async () => { try { const res = await apiFetch(`/api/moders/${id}`, { method: 'DELETE' }); if (res.ok) { loadModers(); showToast('Модератор удален', 'success'); } else { showToast('Ошибка удаления', 'error'); } } catch (e) { showToast('Ошибка удаления', 'error'); } }); } else if (action === 'reset') { const row = btn.closest('tr'); const actionsCell = row.cells[2]; // Если мы уже в режиме редактирования, ничего не делаем if (actionsCell.querySelector('input')) return; // Сохраняем оригинальные кнопки const originalButtons = actionsCell.innerHTML; // Создаем поле ввода и кнопки actionsCell.innerHTML = ` `; const input = actionsCell.querySelector('input'); input.focus(); const saveBtn = actionsCell.querySelector('[data-action="save-pass"]'); const cancelBtn = actionsCell.querySelector('[data-action="cancel-pass"]'); const handleSave = async () => { const newPass = input.value; if (!newPass || newPass.length < 4) { showToast('Пароль должен быть не менее 4 символов', 'error'); return; } try { const res = await apiFetch(`/api/moders/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: newPass }) }); if (res.ok) { showToast('Пароль обновлён', 'success'); actionsCell.innerHTML = originalButtons; // Возвращаем кнопки } else { showToast('Ошибка смены пароля', 'error'); } } catch (e) { showToast('Ошибка смены пароля', 'error'); } }; const handleCancel = () => { actionsCell.innerHTML = originalButtons; }; saveBtn.addEventListener('click', handleSave); cancelBtn.addEventListener('click', handleCancel); } }); } fetchStatus(); fetchConfig(); });