// js/logs.ts - Логика для страницы журнала логов import { drawChart } from './chart.js'; // ============================================ // ТИПЫ // ============================================ interface LogFileInfo { name: string; path: string; size: number; mod_time: string; time: string; date: string; hour: number; is_active: boolean; } interface LogSample { ts: number; time: string; value: number; amplitude: number; signal: number; strength_name: string; } interface LogDataResponse { filename: string; total: number; page: number; page_size: number; total_pages: number; has_previous: boolean; has_next: boolean; start_index: number; end_index: number; samples: LogSample[]; stats: { total_points: number; amplitude_over_0: number; signal_over_10: number; }; order: string; } interface Event { time: string; event: string; } interface EventsResponse { events: Event[]; total: number; page: number; page_size: number; total_pages: number; has_previous: boolean; has_next: boolean; returned: number; start_index: number; end_index: number; order?: string; } // ============================================ // СОСТОЯНИЕ // ============================================ interface AppState { currentFile: string | null; currentData: LogDataResponse | null; currentEvents: Event[]; currentPage: number; totalPages: number; isLoading: boolean; // Для данных dataPage: number; dataTotalPages: number; isLoadingData: boolean; } const state: AppState = { currentFile: null, currentData: null, currentEvents: [], currentPage: 1, totalPages: 1, isLoading: false, dataPage: 1, dataTotalPages: 1, isLoadingData: false, }; const PAGE_SIZE = 100; const DATA_PAGE_SIZE = 100; // ============================================ // DOM ЭЛЕМЕНТЫ // ============================================ const DOM = { fileList: document.getElementById('fileList') as HTMLElement | null, logStats: document.getElementById('logStats') as HTMLElement | null, dataTableContainer: document.getElementById('dataTableContainer') as HTMLElement | null, logChart: document.getElementById('logChart') as HTMLCanvasElement | null, eventsContent: document.getElementById('eventsContent') as HTMLElement | null, serverTime: document.getElementById('server-time') as HTMLElement | null, // Боковая панель logsSidebar: document.querySelector('.logs-sidebar') as HTMLElement | null, logsContainer: document.querySelector('.logs-container') as HTMLElement | null, // Элементы пагинации для данных dataPageInfo: document.getElementById('dataPageInfo') as HTMLElement | null, dataPrevPageBtn: document.getElementById('dataPrevPage') as HTMLButtonElement | null, dataNextPageBtn: document.getElementById('dataNextPage') as HTMLButtonElement | null, dataFirstPageBtn: document.getElementById('dataFirstPage') as HTMLButtonElement | null, dataLastPageBtn: document.getElementById('dataLastPage') as HTMLButtonElement | null, dataPageInput: document.getElementById('dataPageInput') as HTMLInputElement | null, dataGoToPageBtn: document.getElementById('dataGoToPage') as HTMLButtonElement | null, // Элементы пагинации для событий pageInfo: document.getElementById('pageInfo') as HTMLElement | null, prevPageBtn: document.getElementById('prevPage') as HTMLButtonElement | null, nextPageBtn: document.getElementById('nextPage') as HTMLButtonElement | null, firstPageBtn: document.getElementById('firstPage') as HTMLButtonElement | null, lastPageBtn: document.getElementById('lastPage') as HTMLButtonElement | null, pageInput: document.getElementById('pageInput') as HTMLInputElement | null, goToPageBtn: document.getElementById('goToPage') as HTMLButtonElement | null, }; // ============================================ // УПРАВЛЕНИЕ БОКОВОЙ ПАНЕЛЬЮ // ============================================ /** * Показать/скрыть боковую панель в зависимости от активной вкладки */ function toggleSidebar(show: boolean): void { if (!DOM.logsSidebar || !DOM.logsContainer) return; if (show) { // Показываем панель DOM.logsSidebar.style.display = 'block'; DOM.logsSidebar.style.flex = '0 0 280px'; DOM.logsContainer.style.gap = '20px'; } else { // Скрываем панель DOM.logsSidebar.style.display = 'none'; DOM.logsSidebar.style.flex = '0 0 0'; DOM.logsContainer.style.gap = '0'; } } // ============================================ // ФУНКЦИИ ЗАГРУЗКИ ДАННЫХ // ============================================ /** * Загрузка списка файлов */ async function loadFileList(): Promise { try { const response = await fetch('/api/log/files'); const files: LogFileInfo[] = await response.json(); if (!DOM.fileList) return; DOM.fileList.innerHTML = ''; files.forEach((file: LogFileInfo) => { const div = document.createElement('div'); div.className = 'log-file-item'; if (state.currentFile === file.name) { div.classList.add('active'); } div.innerHTML = `
${file.name}
${file.time} | ${(file.size / 1024).toFixed(1)} KB
`; div.addEventListener('click', () => { state.dataPage = 1; void loadFileData(file.name, 1); }); DOM.fileList?.appendChild(div); }); // Автоматически загружаем первый файл if (files.length > 0 && !state.currentFile) { state.dataPage = 1; await loadFileData(files[0].name, 1); } } catch (error) { console.error('[Logs] Ошибка загрузки списка файлов:', error); } } /** * Загрузка данных из файла с пагинацией */ async function loadFileData(filename: string, page: number = 1): Promise { if (state.isLoadingData) return; state.currentFile = filename; state.dataPage = page; // Обновляем активный элемент в списке const items = document.querySelectorAll('.log-file-item'); Array.from(items).forEach((el) => { el.classList.remove('active'); }); const activeItems = document.querySelectorAll('.log-file-item'); for (let i = 0; i < activeItems.length; i++) { const item = activeItems[i] as HTMLElement; if (item.textContent?.includes(filename)) { item.classList.add('active'); break; } } try { state.isLoadingData = true; // Показываем индикатор загрузки if (DOM.dataTableContainer) { DOM.dataTableContainer.innerHTML = '
⏳ Загрузка данных...
'; } const url = `/api/log/data?file=${encodeURIComponent(filename)}&page=${page}&page_size=${DATA_PAGE_SIZE}`; console.log(`[Logs] Загрузка данных: страница ${page} для ${filename}`); const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data: LogDataResponse = await response.json(); state.currentData = data; state.dataTotalPages = data.total_pages; renderData(data); updateDataPagination(data); console.log(`[Logs] Загружено: ${data.samples.length} записей, Страница: ${page}/${data.total_pages}`); } catch (error) { console.error('[Logs] Ошибка загрузки данных:', error); if (DOM.dataTableContainer) { DOM.dataTableContainer.innerHTML = `
Нет данных для отображения
Файл пуст или данные ещё не поступили
`; } if (DOM.dataPageInfo) { DOM.dataPageInfo.textContent = 'Нет записей'; } if (DOM.dataFirstPageBtn) DOM.dataFirstPageBtn.disabled = true; if (DOM.dataPrevPageBtn) DOM.dataPrevPageBtn.disabled = true; if (DOM.dataNextPageBtn) DOM.dataNextPageBtn.disabled = true; if (DOM.dataLastPageBtn) DOM.dataLastPageBtn.disabled = true; } finally { state.isLoadingData = false; } } /** * Загрузка событий с пагинацией */ async function loadEvents(page: number = 1): Promise { if (state.isLoading) return; try { state.isLoading = true; state.currentPage = page; if (DOM.eventsContent) { DOM.eventsContent.innerHTML = '
Загрузка событий...
'; } const url = `/api/log/events?page=${page}&page_size=${PAGE_SIZE}&order=newest_first`; console.log(`[Logs] Загрузка событий: страница ${page}`); const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data: EventsResponse = await response.json(); state.totalPages = data.total_pages; state.currentEvents = data.events || []; renderEvents(data); updatePagination(data); console.log(`[Logs] Загружено: ${data.returned} событий, Страница: ${page}/${data.total_pages}`); } catch (error) { console.error('[Logs] Ошибка загрузки событий:', error); if (DOM.eventsContent) { DOM.eventsContent.innerHTML = `
Нет событий
Журнал событий пуст
`; } } finally { state.isLoading = false; } } // ============================================ // ФУНКЦИИ ОТРИСОВКИ // ============================================ /** * Отрисовка данных файла */ function renderData(data: LogDataResponse): void { const samples = data.samples || []; // Статистика if (DOM.logStats) { const stats = data.stats || { total_points: 0, amplitude_over_0: 0, signal_over_10: 0 }; DOM.logStats.innerHTML = `
${stats.total_points}
Всего точек
${stats.amplitude_over_0}
Амплитуда > 0
${stats.signal_over_10}
Сигнал > 10
`; } // График if (DOM.logChart && data.total > 0) { const history = samples.map((s: LogSample) => s.signal); drawChart(DOM.logChart, history); } // Таблица if (samples.length > 0 && DOM.dataTableContainer) { let html = ``; samples.forEach((s: LogSample, index: number) => { let ampClass = ''; if (s.amplitude === 0) ampClass = 'strength-0'; else if (s.amplitude === 1) ampClass = 'strength-1'; else if (s.amplitude === 2) ampClass = 'strength-2'; else if (s.amplitude === 3) ampClass = 'strength-3'; const num = index + 1; html += ``; }); html += '
# Время Амплитуда Сигнал
${num} ${s.time} ${s.amplitude} ${s.signal}
'; DOM.dataTableContainer.innerHTML = html; } else if (DOM.dataTableContainer) { DOM.dataTableContainer.innerHTML = `
Нет данных
Выберите файл с данными
`; } } /** * Отрисовка событий */ function renderEvents(data: EventsResponse): void { if (!DOM.eventsContent) return; let html = ''; if (data.events.length === 0) { html = `
Нет событий
Журнал событий пуст
`; } else { data.events.forEach((e: Event, index: number) => { const num = index + 1; html += `
#${num} [${e.time}] ${e.event}
`; }); } DOM.eventsContent.innerHTML = html; } // ============================================ // ПАГИНАЦИЯ ДЛЯ ДАННЫХ // ============================================ function updateDataPagination(data: LogDataResponse): void { const { page, total_pages, has_previous, has_next, start_index, end_index, total } = data; if (DOM.dataPageInfo) { if (total === 0) { DOM.dataPageInfo.textContent = 'Нет записей'; } else { DOM.dataPageInfo.textContent = `Страница ${page} из ${total_pages} (записи ${start_index}-${end_index} из ${total}) новые сверху`; } } if (DOM.dataFirstPageBtn) DOM.dataFirstPageBtn.disabled = !has_previous; if (DOM.dataPrevPageBtn) DOM.dataPrevPageBtn.disabled = !has_previous; if (DOM.dataNextPageBtn) DOM.dataNextPageBtn.disabled = !has_next; if (DOM.dataLastPageBtn) DOM.dataLastPageBtn.disabled = !has_next; if (DOM.dataPageInput) { DOM.dataPageInput.value = page.toString(); DOM.dataPageInput.max = total_pages.toString(); } } function goToDataPage(page: number): void { if (page < 1) page = 1; if (page > state.dataTotalPages) page = state.dataTotalPages; if (page === state.dataPage) return; if (!state.currentFile) return; void loadFileData(state.currentFile, page); } function goToDataFirstPage(): void { goToDataPage(1); } function goToDataLastPage(): void { goToDataPage(state.dataTotalPages); } function goToDataPreviousPage(): void { goToDataPage(state.dataPage - 1); } function goToDataNextPage(): void { goToDataPage(state.dataPage + 1); } // ============================================ // ПАГИНАЦИЯ ДЛЯ СОБЫТИЙ // ============================================ function updatePagination(data: EventsResponse): void { const { page, total_pages, has_previous, has_next, start_index, end_index, total } = data; if (DOM.pageInfo) { if (total === 0) { DOM.pageInfo.textContent = 'Нет записей'; } else { DOM.pageInfo.textContent = `Страница ${page} из ${total_pages} (записи ${start_index}-${end_index} из ${total}) новые сверху`; } } if (DOM.firstPageBtn) DOM.firstPageBtn.disabled = !has_previous; if (DOM.prevPageBtn) DOM.prevPageBtn.disabled = !has_previous; if (DOM.nextPageBtn) DOM.nextPageBtn.disabled = !has_next; if (DOM.lastPageBtn) DOM.lastPageBtn.disabled = !has_next; if (DOM.pageInput) { DOM.pageInput.value = page.toString(); DOM.pageInput.max = total_pages.toString(); } } function goToPage(page: number): void { if (page < 1) page = 1; if (page > state.totalPages) page = state.totalPages; if (page === state.currentPage) return; void loadEvents(page); } function goToFirstPage(): void { goToPage(1); } function goToLastPage(): void { goToPage(state.totalPages); } function goToPreviousPage(): void { goToPage(state.currentPage - 1); } function goToNextPage(): void { goToPage(state.currentPage + 1); } // ============================================ // ОБНОВЛЕНИЕ ВРЕМЕНИ // ============================================ async function updateTime(): Promise { try { const response = await fetch('/api/health'); const data = await response.json(); if (DOM.serverTime) { DOM.serverTime.textContent = data.server_time || new Date().toLocaleTimeString(); } } catch (error) { if (DOM.serverTime) { DOM.serverTime.textContent = new Date().toLocaleTimeString(); } } } // ============================================ // ИНИЦИАЛИЗАЦИЯ // ============================================ function initDataPaginationControls(): void { const dataTab = document.getElementById('tab-data'); if (!dataTab) return; let paginationContainer = document.getElementById('dataPaginationContainer'); if (!paginationContainer) { const tableContainer = document.getElementById('dataTableContainer'); if (!tableContainer) return; paginationContainer = document.createElement('div'); paginationContainer.id = 'dataPaginationContainer'; paginationContainer.className = 'pagination-container'; paginationContainer.innerHTML = `
Страница 1 из 1
`; dataTab.insertBefore(paginationContainer, tableContainer); } DOM.dataFirstPageBtn = document.getElementById('dataFirstPage') as HTMLButtonElement | null; DOM.dataPrevPageBtn = document.getElementById('dataPrevPage') as HTMLButtonElement | null; DOM.dataNextPageBtn = document.getElementById('dataNextPage') as HTMLButtonElement | null; DOM.dataLastPageBtn = document.getElementById('dataLastPage') as HTMLButtonElement | null; DOM.dataPageInfo = document.getElementById('dataPageInfo') as HTMLElement | null; DOM.dataPageInput = document.getElementById('dataPageInput') as HTMLInputElement | null; DOM.dataGoToPageBtn = document.getElementById('dataGoToPage') as HTMLButtonElement | null; if (DOM.dataFirstPageBtn) DOM.dataFirstPageBtn.addEventListener('click', goToDataFirstPage); if (DOM.dataPrevPageBtn) DOM.dataPrevPageBtn.addEventListener('click', goToDataPreviousPage); if (DOM.dataNextPageBtn) DOM.dataNextPageBtn.addEventListener('click', goToDataNextPage); if (DOM.dataLastPageBtn) DOM.dataLastPageBtn.addEventListener('click', goToDataLastPage); if (DOM.dataGoToPageBtn) { DOM.dataGoToPageBtn.addEventListener('click', () => { if (DOM.dataPageInput) { const page = parseInt(DOM.dataPageInput.value); if (!isNaN(page) && page >= 1) { goToDataPage(page); } } }); } if (DOM.dataPageInput) { DOM.dataPageInput.addEventListener('keypress', (e: KeyboardEvent) => { if (e.key === 'Enter' && DOM.dataGoToPageBtn) { DOM.dataGoToPageBtn.click(); } }); } } function initEventsPaginationControls(): void { const eventsTab = document.getElementById('tab-events'); if (!eventsTab) return; let paginationContainer = document.getElementById('eventsPaginationContainer'); if (!paginationContainer) { const eventsContent = document.getElementById('eventsContent'); if (!eventsContent) return; paginationContainer = document.createElement('div'); paginationContainer.id = 'eventsPaginationContainer'; paginationContainer.className = 'pagination-container'; paginationContainer.innerHTML = `
Страница 1 из 1
`; eventsTab.insertBefore(paginationContainer, eventsContent); } DOM.firstPageBtn = document.getElementById('firstPage') as HTMLButtonElement | null; DOM.prevPageBtn = document.getElementById('prevPage') as HTMLButtonElement | null; DOM.nextPageBtn = document.getElementById('nextPage') as HTMLButtonElement | null; DOM.lastPageBtn = document.getElementById('lastPage') as HTMLButtonElement | null; DOM.pageInfo = document.getElementById('pageInfo') as HTMLElement | null; DOM.pageInput = document.getElementById('pageInput') as HTMLInputElement | null; DOM.goToPageBtn = document.getElementById('goToPage') as HTMLButtonElement | null; if (DOM.firstPageBtn) DOM.firstPageBtn.addEventListener('click', goToFirstPage); if (DOM.prevPageBtn) DOM.prevPageBtn.addEventListener('click', goToPreviousPage); if (DOM.nextPageBtn) DOM.nextPageBtn.addEventListener('click', goToNextPage); if (DOM.lastPageBtn) DOM.lastPageBtn.addEventListener('click', goToLastPage); if (DOM.goToPageBtn) { DOM.goToPageBtn.addEventListener('click', () => { if (DOM.pageInput) { const page = parseInt(DOM.pageInput.value); if (!isNaN(page) && page >= 1) { goToPage(page); } } }); } if (DOM.pageInput) { DOM.pageInput.addEventListener('keypress', (e: KeyboardEvent) => { if (e.key === 'Enter' && DOM.goToPageBtn) { DOM.goToPageBtn.click(); } }); } } function initTabs(): void { const tabs = document.querySelectorAll('.tab-btn[data-tab]'); tabs.forEach((btn) => { btn.addEventListener('click', () => { tabs.forEach((b) => b.classList.remove('active')); btn.classList.add('active'); const tabName = btn.getAttribute('data-tab'); const contents = document.querySelectorAll('.tab-content'); contents.forEach((el) => el.classList.remove('active')); const targetTab = document.getElementById(`tab-${tabName}`); if (targetTab) targetTab.classList.add('active'); // ===== УПРАВЛЕНИЕ БОКОВОЙ ПАНЕЛЬЮ ===== if (tabName === 'data') { // Показываем боковую панель для вкладки "Данные" toggleSidebar(true); // Если данные ещё не загружены, загружаем if (!state.currentData) { const firstFile = document.querySelector('.log-file-item'); if (firstFile) { const fileName = firstFile.querySelector('.file-name')?.textContent; if (fileName) { void loadFileData(fileName, 1); } } } } else if (tabName === 'events') { // Скрываем боковую панель для вкладки "События" toggleSidebar(false); // Загружаем события, если их нет if (state.currentEvents.length === 0) { void loadEvents(1); } } }); }); } async function init(): Promise { console.log('[Logs] Инициализация...'); // По умолчанию показываем боковую панель toggleSidebar(true); initDataPaginationControls(); initEventsPaginationControls(); initTabs(); await loadFileList(); await updateTime(); setInterval(() => { void updateTime(); }, 1000); // Проверяем, какая вкладка активна при загрузке const dataTab = document.getElementById('tab-data'); const eventsTab = document.getElementById('tab-events'); if (eventsTab && eventsTab.classList.contains('active')) { toggleSidebar(false); await loadEvents(1); } else { toggleSidebar(true); } console.log('[Logs] Инициализация завершена'); } // ============================================ // ЗАПУСК // ============================================ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { void init(); }); } else { void init(); }