From 35caa2bb72f375e5bbfb17a5249136c866173c73 Mon Sep 17 00:00:00 2001 From: Maxim Date: Thu, 18 Jun 2026 11:01:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=9F=D0=B0=D0=B3=D0=B8=D0=BD=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=B2=20=D0=BB=D0=BE=D0=B3=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/main.go | 142 +++++++--- cmd/server/web/css/logs.css | 128 +++++++++ cmd/server/web/js/logs.ts | 514 ++++++++++++++++++++++++++++++++++++ cmd/server/web/logs.html | 204 +------------- 4 files changed, 745 insertions(+), 243 deletions(-) create mode 100644 cmd/server/web/js/logs.ts diff --git a/cmd/server/main.go b/cmd/server/main.go index f1673f6..b03e3fe 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -106,58 +106,112 @@ func (a *API) HandleLogFiles(w http.ResponseWriter, r *http.Request) { // HandleLogEvents - чтение событий func (a *API) HandleLogEvents(w http.ResponseWriter, r *http.Request) { - limit := 100 - if l := r.URL.Query().Get("limit"); l != "" { - if parsed, err := strconv.Atoi(l); err == nil { - limit = parsed - } - } + // Параметры пагинации + page := 1 + if p := r.URL.Query().Get("page"); p != "" { + if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 { + page = parsed + } + } - eventsPath, err := logger.GetEventHumanLogPath() - if err != nil { - http.Error(w, err.Error(), 500) - return - } + pageSize := 100 + if ps := r.URL.Query().Get("page_size"); ps != "" { + if parsed, err := strconv.Atoi(ps); err == nil && parsed > 0 { + pageSize = parsed + } + } - content, err := os.ReadFile(eventsPath) - if err != nil { - http.Error(w, err.Error(), 500) - return - } + eventsPath, err := logger.GetEventHumanLogPath() + if err != nil { + http.Error(w, err.Error(), 500) + return + } - lines := strings.Split(string(content), "\n") - // Берем последние N строк - if len(lines) > limit+1 { - lines = lines[len(lines)-limit-1:] - } + content, err := os.ReadFile(eventsPath) + if err != nil { + http.Error(w, err.Error(), 500) + return + } - // Парсим строки для JSON - type Event struct { - Time string `json:"time"` - Event string `json:"event"` - } + // Разбиваем на строки + lines := strings.Split(string(content), "\n") - var events []Event - for _, line := range lines { - if line == "" { - continue - } - // Формат: [2026-06-16 11:23:45.123] EVENT: СЕРВЕР_ЗАПУЩЕН - parts := strings.SplitN(line, "] EVENT: ", 2) - if len(parts) == 2 { - events = append(events, Event{ - Time: strings.TrimPrefix(parts[0], "["), - Event: parts[1], - }) - } - } + // Удаляем пустые строки + var nonEmptyLines []string + for _, line := range lines { + if line != "" { + nonEmptyLines = append(nonEmptyLines, line) + } + } + lines = nonEmptyLines - writeJSON(w, map[string]any{ - "events": events, - "total": len(events), - }) + total := len(lines) + totalPages := (total + pageSize - 1) / pageSize + + if totalPages == 0 { + totalPages = 1 + } + + // Корректируем номер страницы + if page > totalPages { + page = totalPages + } + if page < 1 { + page = 1 + } + + // Вычисляем смещение (нумерация страниц с 1) + // Страница 1 → самые ранние записи (начало файла) + // Страница N → самые поздние записи (конец файла) + start := (page - 1) * pageSize + end := start + pageSize + if end > total { + end = total + } + + // Получаем строки для текущей страницы + var resultLines []string + if start < total { + resultLines = lines[start:end] + } else { + resultLines = []string{} + } + + // Парсим строки для JSON + type Event struct { + Time string `json:"time"` + Event string `json:"event"` + } + + var events []Event + for _, line := range resultLines { + if line == "" { + continue + } + parts := strings.SplitN(line, "] EVENT: ", 2) + if len(parts) == 2 { + events = append(events, Event{ + Time: strings.TrimPrefix(parts[0], "["), + Event: parts[1], + }) + } + } + + writeJSON(w, map[string]any{ + "events": events, + "total": total, + "page": page, + "page_size": pageSize, + "total_pages": totalPages, + "has_previous": page > 1, + "has_next": page < totalPages, + "returned": len(events), + "start_index": start + 1, + "end_index": end, + }) } + // HandleLogData - чтение данных из конкретного bin файла func (a *API) HandleLogData(w http.ResponseWriter, r *http.Request) { filename := r.URL.Query().Get("file") diff --git a/cmd/server/web/css/logs.css b/cmd/server/web/css/logs.css index 6ce49e2..2dc3952 100644 --- a/cmd/server/web/css/logs.css +++ b/cmd/server/web/css/logs.css @@ -139,4 +139,132 @@ .tab-content.active { display: block; +} + +/* ===== ПАГИНАЦИЯ ===== */ +.pagination-container { + margin: 16px 0; + padding: 12px; + border-top: 1px solid rgba(0, 255, 136, 0.2); + border-bottom: 1px solid rgba(0, 255, 136, 0.2); +} + +.pagination-controls { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + flex-wrap: wrap; +} + +.pagination-btn { + padding: 6px 14px; + background: rgba(0, 255, 136, 0.1); + color: #00ff88; + border: 1px solid #00ff88; + border-radius: 4px; + cursor: pointer; + font-family: monospace; + font-size: 14px; + transition: all 0.2s; + min-width: 40px; +} + +.pagination-btn:hover:not(:disabled) { + background: rgba(0, 255, 136, 0.3); + transform: scale(1.05); +} + +.pagination-btn:disabled { + opacity: 0.3; + cursor: not-allowed; + transform: none; +} + +.pagination-info { + padding: 0 12px; + color: #00ff88; + font-size: 14px; + font-family: monospace; + min-width: 200px; + text-align: center; +} + +.pagination-goto { + display: flex; + align-items: center; + gap: 4px; + margin-left: 8px; +} + +.pagination-input { + width: 60px; + padding: 4px 8px; + background: rgba(0, 0, 0, 0.6); + color: #00ff88; + border: 1px solid #00ff88; + border-radius: 4px; + font-family: monospace; + font-size: 14px; + text-align: center; +} + +.pagination-input:focus { + outline: none; + box-shadow: 0 0 10px rgba(0, 255, 136, 0.3); +} + +.pagination-goto-btn { + padding: 4px 12px; + font-size: 12px; +} + +.loading-indicator { + text-align: center; + padding: 20px; + color: #00ff88; + opacity: 0.7; + font-family: monospace; +} + +/* ===== АДАПТИВНОСТЬ ===== */ +@media (max-width: 768px) { + .pagination-controls { + gap: 4px; + } + + .pagination-btn { + padding: 4px 10px; + font-size: 12px; + min-width: 32px; + } + + .pagination-info { + font-size: 12px; + min-width: 120px; + padding: 0 8px; + } + + .pagination-goto { + margin-left: 4px; + } + + .pagination-input { + width: 50px; + font-size: 12px; + padding: 3px 6px; + } +} + +@media (max-width: 500px) { + .pagination-controls { + flex-wrap: wrap; + justify-content: center; + } + + .pagination-info { + order: -1; + width: 100%; + margin-bottom: 4px; + } } \ No newline at end of file diff --git a/cmd/server/web/js/logs.ts b/cmd/server/web/js/logs.ts new file mode 100644 index 0000000..9bdf8e6 --- /dev/null +++ b/cmd/server/web/js/logs.ts @@ -0,0 +1,514 @@ +// 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; + count: number; + strength: number; + strength_name: string; + amplitude: number; + signal: number; +} + +interface LogDataResponse { + filename: string; + total: number; + samples: LogSample[]; +} + +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; +} + +// ============================================ +// СОСТОЯНИЕ +// ============================================ + +interface AppState { + currentFile: string | null; + currentData: LogDataResponse | null; + currentEvents: Event[]; + currentPage: number; + totalPages: number; + isLoading: boolean; +} + +const state: AppState = { + currentFile: null, + currentData: null, + currentEvents: [], + currentPage: 1, + totalPages: 1, + isLoading: false, +}; + +const 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, + + // Элементы пагинации + 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, +}; + +// ============================================ +// ФУНКЦИИ ЗАГРУЗКИ ДАННЫХ +// ============================================ + +/** + * Загрузка списка файлов + */ +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', () => loadFileData(file.name)); + DOM.fileList?.appendChild(div); + }); + + // Автоматически загружаем первый файл + if (files.length > 0 && !state.currentFile) { + await loadFileData(files[0].name); + } + } catch (error) { + console.error('[Logs] Ошибка загрузки списка файлов:', error); + } +} + +/** + * Загрузка данных из файла + */ +async function loadFileData(filename: string): Promise { + state.currentFile = filename; + + // Обновляем активный элемент в списке + 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; + if (item.textContent?.includes(filename)) { + item.classList.add('active'); + break; + } + } + + try { + const response = await fetch(`/api/log/data?file=${encodeURIComponent(filename)}`); + const data: LogDataResponse = await response.json(); + state.currentData = data; + + renderData(data); + } catch (error) { + console.error('[Logs] Ошибка загрузки данных:', error); + } +} + +/** + * Загрузка событий с пагинацией + */ +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}`; + console.log(`[Logs] Загрузка страницы ${page}`); + + const response = await fetch(url); + 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 (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); + + DOM.logStats.innerHTML = ` +
+
${totalPoints}
+
Всего точек
+
+
+
${amplitudeOver0Count}
+
Амплитуда > 0
+
+
+
${signalOver10Count}
+
Сигнал > 10
+
+
+
${totalObjects}
+
Объектов всего
+
+ `; + } + + // График + if (DOM.logChart && samples.length > 0) { + const history = samples.map((s: LogSample) => s.signal); + drawChart(DOM.logChart, history); + } + + // Таблица + if (samples.length > 0 && DOM.dataTableContainer) { + let html = ` + + + + + + + + `; + + const displaySamples = samples.slice(-200); + displaySamples.forEach((s: LogSample) => { + 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'; + + html += ` + + + + `; + }); + + html += '
ВремяАмплитудаСигнал
${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) => { + html += `
[${e.time}] ${e.event}
`; + }); + } + DOM.eventsContent.innerHTML = html; +} + +/** + * Обновление элементов пагинации + */ +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; + + 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 initPaginationControls(): void { + // Создаём элементы пагинации, если их нет + const eventsTab = document.getElementById('tab-events'); + if (!eventsTab) return; + + // Проверяем, есть ли уже контейнер пагинации + let paginationContainer = document.getElementById('paginationContainer'); + if (!paginationContainer) { + paginationContainer = document.createElement('div'); + paginationContainer.id = 'paginationContainer'; + paginationContainer.className = 'pagination-container'; + paginationContainer.innerHTML = ` +
+ + + Страница 1 из 1 + + + + + + +
+ `; + + // Вставляем перед содержимым событий + const eventsContent = document.getElementById('eventsContent'); + if (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; + 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 === 'events') { + // Загружаем первую страницу событий + if (state.currentEvents.length === 0) { + loadEvents(1); + } + } + }); + }); +} + +function init(): void { + console.log('[Logs] Инициализация...'); + + // Инициализация пагинации + initPaginationControls(); + + // Инициализация табов + initTabs(); + + // Загрузка списка файлов + loadFileList(); + + // Обновление времени + updateTime(); + setInterval(updateTime, 1000); + + // Если вкладка "События" активна по умолчанию, загружаем события + const eventsTab = document.getElementById('tab-events'); + if (eventsTab && eventsTab.classList.contains('active')) { + loadEvents(1); + } + + console.log('[Logs] Инициализация завершена'); +} + +// ============================================ +// ЗАПУСК +// ============================================ + +// Запускаем инициализацию после загрузки DOM +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} else { + init(); +} \ No newline at end of file diff --git a/cmd/server/web/logs.html b/cmd/server/web/logs.html index e523ca1..c41d231 100644 --- a/cmd/server/web/logs.html +++ b/cmd/server/web/logs.html @@ -21,9 +21,9 @@
-

ЖУРНАЛ ЛОГОВ

+

📋 ЖУРНАЛ ЛОГОВ

--:--:--
- ← На главную + ← В режим работы
@@ -47,208 +47,14 @@
+
Загрузка...
- + + \ No newline at end of file