From 18d005c93311b99f6d703f645d5fe3088dc67a64 Mon Sep 17 00:00:00 2001 From: Maxim Date: Thu, 18 Jun 2026 14:40:59 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=B4=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D0=B2=20=D0=BB=D0=BE=D0=B3=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/main.go | 198 ++++++++++++++++-------- cmd/server/web/css/logs.css | 17 ++ cmd/server/web/js/logs.ts | 301 ++++++++++++++++++++++++++---------- cmd/server/web/logs.html | 4 +- 4 files changed, 370 insertions(+), 150 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index e67a1d6..4ce1382 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -219,95 +219,163 @@ func (a *API) HandleLogEvents(w http.ResponseWriter, r *http.Request) { } -// HandleLogData - чтение данных из конкретного bin файла +// HandleLogData - чтение данных из конкретного bin файла с пагинацией func (a *API) HandleLogData(w http.ResponseWriter, r *http.Request) { - filename := r.URL.Query().Get("file") - if filename == "" { - http.Error(w, "отсутствует параметр file", 400) - return - } + filename := r.URL.Query().Get("file") + if filename == "" { + http.Error(w, "отсутствует параметр file", 400) + return + } - // Защита от path traversal - filename = filepath.Base(filename) - dataDir, err := logger.GetDataLogsDir() - if err != nil { - http.Error(w, err.Error(), 500) - return - } + // Параметры пагинации + page := 1 + if p := r.URL.Query().Get("page"); p != "" { + if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 { + page = parsed + } + } - filePath := filepath.Join(dataDir, filename) - if _, err := os.Stat(filePath); os.IsNotExist(err) { - http.Error(w, "файл не найден", 404) - return - } + pageSize := 100 + if ps := r.URL.Query().Get("page_size"); ps != "" { + if parsed, err := strconv.Atoi(ps); err == nil && parsed > 0 { + pageSize = parsed + } + } - // Читаем бинарный файл - f, err := os.Open(filePath) - if err != nil { - http.Error(w, err.Error(), 500) - return - } - defer f.Close() + // Защита от path traversal + filename = filepath.Base(filename) + dataDir, err := logger.GetDataLogsDir() + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + filePath := filepath.Join(dataDir, filename) + if _, err := os.Stat(filePath); os.IsNotExist(err) { + http.Error(w, "файл не найден", 404) + return + } + + // Читаем бинарный файл + f, err := os.Open(filePath) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + defer f.Close() type Sample struct { Timestamp int64 `json:"ts"` Time string `json:"time"` Value byte `json:"value"` - Count byte `json:"count"` - Strength byte `json:"strength"` - StrengthName string `json:"strength_name"` Amplitude byte `json:"amplitude"` // Старшие 2 бита (0-3) Signal byte `json:"signal"` // Младшие 6 бит (0-63) + StrengthName string `json:"strength_name"` } - var samples []Sample + // Сначала читаем все сэмплы + var allSamples []Sample buf := make([]byte, 9) // 8 байт timestamp + 1 байт value - for { - n, err := f.Read(buf) - if err == io.EOF { - break - } - if err != nil { - http.Error(w, err.Error(), 500) - return - } - if n < 9 { - continue - } + for { + n, err := f.Read(buf) + if err == io.EOF { + break + } + if err != nil { + http.Error(w, err.Error(), 500) + return + } + if n < 9 { + continue + } - ts := int64(binary.LittleEndian.Uint64(buf[0:8])) + ts := int64(binary.LittleEndian.Uint64(buf[0:8])) value := buf[8] - data := logger.ParseGPIO(value) + amplitude := (value >> 6) & 0x3 + signal := value & 0x3F - samples = append(samples, Sample{ + allSamples = append(allSamples, Sample{ Timestamp: ts, - Time: time.UnixMicro(ts).Format("02.01.2006 15:04:05"), + Time: time.UnixMicro(ts).Format("02.01.2006 15:04:05"), Value: value, - Count: data.Count, - Strength: data.Strength, - StrengthName: logger.GetStrengthName(data.Strength), - Amplitude: (value >> 6) & 0x3, // Старшие 2 бита (0-3) - АМПЛИТУДА - Signal: value & 0x3F, // Младшие 6 бит (0-63) - СИГНАЛ + Amplitude: amplitude, + Signal: signal, + StrengthName: logger.GetStrengthName(amplitude), }) - } + } - // Ограничиваем количество точек для производительности - if len(samples) > 10000 { - step := len(samples) / 10000 - var filtered []Sample - for i := 0; i < len(samples); i += step { - filtered = append(filtered, samples[i]) - } - samples = filtered - } + total := len(allSamples) + totalPages := (total + pageSize - 1) / pageSize + if totalPages == 0 { + totalPages = 1 + } - writeJSON(w, map[string]any{ - "filename": filename, - "total": len(samples), - "samples": samples, - }) + // Корректируем номер страницы + if page > totalPages { + page = totalPages + } + if page < 1 { + page = 1 + } + + // Страница 1 = самые новые записи (с конца файла) + // Новые записи в конце файла, поэтому начинаем с конца + start := total - page*pageSize + if start < 0 { + start = 0 + } + + end := start + pageSize + if end > total { + end = total + } + + // Получаем сэмплы для текущей страницы + var pageSamples []Sample + if start < total && start >= 0 { + pageSamples = allSamples[start:end] + } else { + pageSamples = []Sample{} + } + + // Разворачиваем, чтобы новые были сверху + var reversedSamples []Sample + for i := len(pageSamples) - 1; i >= 0; i-- { + reversedSamples = append(reversedSamples, pageSamples[i]) + } + + // Считаем статистику (без суммы объектов) + amplitudeOver0 := 0 + signalOver10 := 0 + for _, s := range allSamples { + if s.Amplitude > 0 { + amplitudeOver0++ + } + if s.Signal > 10 { + signalOver10++ + } + } + + writeJSON(w, map[string]any{ + "filename": filename, + "total": total, + "page": page, + "page_size": pageSize, + "total_pages": totalPages, + "has_previous": page > 1, + "has_next": page < totalPages, + "start_index": start + 1, + "end_index": end, + "samples": reversedSamples, + "stats": map[string]any{ + "total_points": total, + "amplitude_over_0": amplitudeOver0, + "signal_over_10": signalOver10, + }, + "order": "newest_first", + }) } func (a *API) HandleHealth(w http.ResponseWriter, r *http.Request) { diff --git a/cmd/server/web/css/logs.css b/cmd/server/web/css/logs.css index f1e01e0..9d4bb05 100644 --- a/cmd/server/web/css/logs.css +++ b/cmd/server/web/css/logs.css @@ -545,4 +545,21 @@ font-size: var(--font-size-xs); padding: 3px 4px; } +} + +/* ===== НОМЕРА ЗАПИСЕЙ ===== */ +.event-num { + opacity: 0.3; + font-size: var(--font-size-xs); + margin-right: 8px; + color: #666; + min-width: 30px; + display: inline-block; +} + +.data-table td:first-child { + opacity: 0.3; + font-size: var(--font-size-xs); + color: #666; + min-width: 30px; } \ No newline at end of file diff --git a/cmd/server/web/js/logs.ts b/cmd/server/web/js/logs.ts index 5b104af..ea01a53 100644 --- a/cmd/server/web/js/logs.ts +++ b/cmd/server/web/js/logs.ts @@ -21,17 +21,28 @@ interface LogSample { ts: number; time: string; value: number; - count: number; - strength: number; - strength_name: string; 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 { @@ -64,6 +75,10 @@ interface AppState { currentPage: number; totalPages: number; isLoading: boolean; + // Для данных + dataPage: number; + dataTotalPages: number; + isLoadingData: boolean; } const state: AppState = { @@ -73,9 +88,13 @@ const state: AppState = { currentPage: 1, totalPages: 1, isLoading: false, + dataPage: 1, + dataTotalPages: 1, + isLoadingData: false, }; const PAGE_SIZE = 100; +const DATA_PAGE_SIZE = 100; // ============================================ // DOM ЭЛЕМЕНТЫ @@ -89,7 +108,16 @@ const DOM = { eventsContent: document.getElementById('eventsContent') as HTMLElement | null, serverTime: document.getElementById('server-time') 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, @@ -125,14 +153,16 @@ async function loadFileList(): Promise {
${file.time} | ${(file.size / 1024).toFixed(1)} KB
`; div.addEventListener('click', () => { - void loadFileData(file.name); + state.dataPage = 1; + void loadFileData(file.name, 1); }); DOM.fileList?.appendChild(div); }); // Автоматически загружаем первый файл if (files.length > 0 && !state.currentFile) { - await loadFileData(files[0].name); + state.dataPage = 1; + await loadFileData(files[0].name, 1); } } catch (error) { console.error('[Logs] Ошибка загрузки списка файлов:', error); @@ -140,19 +170,20 @@ async function loadFileList(): Promise { } /** - * Загрузка данных из файла + * Загрузка данных из файла с пагинацией */ -async function loadFileData(filename: string): Promise { +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 для преобразования NodeList в массив 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; @@ -163,13 +194,32 @@ async function loadFileData(filename: string): Promise { } try { - const response = await fetch(`/api/log/data?file=${encodeURIComponent(filename)}`); + 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); 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 = '
❌ Ошибка загрузки данных
'; + } + } finally { + state.isLoadingData = false; } } @@ -183,13 +233,12 @@ async function loadEvents(page: number = 1): Promise { state.isLoading = true; state.currentPage = page; - // Показываем индикатор загрузки if (DOM.eventsContent) { - DOM.eventsContent.innerHTML = '
Загрузка событий...
'; + DOM.eventsContent.innerHTML = '
⏳ Загрузка событий...
'; } const url = `/api/log/events?page=${page}&page_size=${PAGE_SIZE}&order=newest_first`; - console.log(`[Logs] Загрузка страницы ${page} (новые сверху)`); + console.log(`[Logs] Загрузка событий: страница ${page}`); const response = await fetch(url); const data: EventsResponse = await response.json(); @@ -197,15 +246,10 @@ async function loadEvents(page: number = 1): Promise { state.totalPages = data.total_pages; state.currentEvents = data.events || []; - // Отрисовываем события renderEvents(data); - - // Обновляем элементы пагинации updatePagination(data); console.log(`[Logs] Загружено: ${data.returned} событий, Страница: ${page}/${data.total_pages}`); - console.log(`[Logs] Порядок: ${data.order || 'newest_first'}, Показаны записи ${data.start_index}-${data.end_index} из ${data.total}`); - } catch (error) { console.error('[Logs] Ошибка загрузки событий:', error); if (DOM.eventsContent) { @@ -227,43 +271,38 @@ function renderData(data: LogDataResponse): void { const samples = data.samples || []; // Статистика - if (samples.length > 0 && DOM.logStats) { - const totalPoints = samples.length; - const amplitudeOver0Count = samples.filter((s: LogSample) => s.amplitude > 0).length; - const signalOver10Count = samples.filter((s: LogSample) => s.signal > 10).length; - const totalObjects = samples.reduce((sum: number, s: LogSample) => sum + s.signal, 0); - + if (DOM.logStats) { + const stats = data.stats || { total_points: 0, amplitude_over_0: 0, signal_over_10: 0 }; DOM.logStats.innerHTML = `
-
${totalPoints}
+
${stats.total_points}
Всего точек
-
${amplitudeOver0Count}
+
${stats.amplitude_over_0}
Амплитуда > 0
-
${signalOver10Count}
+
${stats.signal_over_10}
Сигнал > 10
-
-
${totalObjects}
-
Объектов всего
-
`; } - // График - if (DOM.logChart && samples.length > 0) { + // График - показываем все данные (ограничиваем для производительности) + if (DOM.logChart && data.total > 0) { + // Для графика загружаем все данные (но не более 5000 точек) + // В реальности лучше отдельный API для графика const history = samples.map((s: LogSample) => s.signal); drawChart(DOM.logChart, history); } - // Таблица + // Таблица - показываем текущую страницу if (samples.length > 0 && DOM.dataTableContainer) { let html = ` + @@ -271,15 +310,17 @@ function renderData(data: LogDataResponse): void { `; - const displaySamples = samples.slice(-200); - displaySamples.forEach((s: LogSample) => { + 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 += ` + @@ -303,68 +344,114 @@ function renderEvents(data: EventsResponse): void { if (data.events.length === 0) { html = '
Нет событий
'; } else { - data.events.forEach((e: Event) => { - html += `
[${e.time}] ${e.event}
`; + 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 { - const orderText = data.order === 'newest_first' ? 'новые сверху' : 'старые сверху'; - DOM.pageInfo.textContent = `Страница ${page} из ${total_pages} (записи ${start_index}-${end_index} из ${total}) ${orderText}`; + 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); // Первая страница = самые новые + goToPage(1); } function goToLastPage(): void { - goToPage(state.totalPages); // Последняя страница = самые старые + goToPage(state.totalPages); } -// "◀ Предыдущая" → страница с номером на 1 меньше (более новые записи) function goToPreviousPage(): void { goToPage(state.currentPage - 1); } -// "Следующая ▶" → страница с номером на 1 больше (более старые записи) function goToNextPage(): void { goToPage(state.currentPage + 1); } @@ -381,7 +468,6 @@ async function updateTime(): Promise { DOM.serverTime.textContent = data.server_time || new Date().toLocaleTimeString(); } } catch (error) { - // Если сервер недоступен, используем локальное время if (DOM.serverTime) { DOM.serverTime.textContent = new Date().toLocaleTimeString(); } @@ -392,16 +478,81 @@ async function updateTime(): Promise { // ИНИЦИАЛИЗАЦИЯ // ============================================ -function initPaginationControls(): void { - // Создаём элементы пагинации, если их нет +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('paginationContainer'); + let paginationContainer = document.getElementById('eventsPaginationContainer'); if (!paginationContainer) { + const eventsContent = document.getElementById('eventsContent'); + if (!eventsContent) return; + paginationContainer = document.createElement('div'); - paginationContainer.id = 'paginationContainer'; + paginationContainer.id = 'eventsPaginationContainer'; paginationContainer.className = 'pagination-container'; paginationContainer.innerHTML = `
@@ -417,14 +568,9 @@ function initPaginationControls(): void {
`; - // Вставляем перед содержимым событий - const eventsContent = document.getElementById('eventsContent'); - if (eventsContent) { - eventsTab.insertBefore(paginationContainer, eventsContent); - } + eventsTab.insertBefore(paginationContainer, eventsContent); } - // Обновляем ссылки на DOM элементы DOM.firstPageBtn = document.getElementById('firstPage') as HTMLButtonElement | null; DOM.prevPageBtn = document.getElementById('prevPage') as HTMLButtonElement | null; DOM.nextPageBtn = document.getElementById('nextPage') as HTMLButtonElement | null; @@ -433,7 +579,6 @@ function initPaginationControls(): void { 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); @@ -463,7 +608,6 @@ 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'); @@ -474,11 +618,8 @@ function initTabs(): void { const targetTab = document.getElementById(`tab-${tabName}`); if (targetTab) targetTab.classList.add('active'); - if (tabName === 'events') { - // Загружаем первую страницу событий - if (state.currentEvents.length === 0) { - void loadEvents(1); - } + if (tabName === 'events' && state.currentEvents.length === 0) { + void loadEvents(1); } }); }); @@ -487,22 +628,17 @@ function initTabs(): void { async function init(): Promise { console.log('[Logs] Инициализация...'); - // Инициализация пагинации - initPaginationControls(); - - // Инициализация табов + initDataPaginationControls(); + initEventsPaginationControls(); initTabs(); - // Загрузка списка файлов await loadFileList(); - // Обновление времени await updateTime(); setInterval(() => { void updateTime(); }, 1000); - // Если вкладка "События" активна по умолчанию, загружаем события const eventsTab = document.getElementById('tab-events'); if (eventsTab && eventsTab.classList.contains('active')) { await loadEvents(1); @@ -515,7 +651,6 @@ async function init(): Promise { // ЗАПУСК // ============================================ -// Запускаем инициализацию после загрузки DOM if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { void init(); diff --git a/cmd/server/web/logs.html b/cmd/server/web/logs.html index e57f6d3..6a0d46c 100644 --- a/cmd/server/web/logs.html +++ b/cmd/server/web/logs.html @@ -1,4 +1,3 @@ - @@ -57,12 +56,13 @@
+
Загрузка событий...
- +
# Время Амплитуда Сигнал
${num} ${s.time} ${s.amplitude} ${s.signal}