Files
go-service/cmd/server/web/js/logs.ts

525 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
order?: string;
}
// ============================================
// СОСТОЯНИЕ
// ============================================
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<void> {
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 = `
<div class="file-name">${file.name}</div>
<div class="file-info">${file.time} | ${(file.size / 1024).toFixed(1)} KB</div>
`;
div.addEventListener('click', () => {
void 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<void> {
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<void> {
if (state.isLoading) return;
try {
state.isLoading = true;
state.currentPage = page;
// Показываем индикатор загрузки
if (DOM.eventsContent) {
DOM.eventsContent.innerHTML = '<div class="loading-indicator">Загрузка событий...</div>';
}
const url = `/api/log/events?page=${page}&page_size=${PAGE_SIZE}&order=newest_first`;
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}`);
console.log(`[Logs] Порядок: ${data.order || 'newest_first'}, Показаны записи ${data.start_index}-${data.end_index} из ${data.total}`);
} catch (error) {
console.error('[Logs] Ошибка загрузки событий:', error);
if (DOM.eventsContent) {
DOM.eventsContent.innerHTML = '<div style="color:#ff4444;">❌ Ошибка загрузки событий</div>';
}
} 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 = `
<div class="log-stat-card">
<div class="value">${totalPoints}</div>
<div class="label">Всего точек</div>
</div>
<div class="log-stat-card">
<div class="value">${amplitudeOver0Count}</div>
<div class="label">Амплитуда > 0</div>
</div>
<div class="log-stat-card">
<div class="value">${signalOver10Count}</div>
<div class="label">Сигнал > 10</div>
</div>
<div class="log-stat-card">
<div class="value">${totalObjects}</div>
<div class="label">Объектов всего</div>
</div>
`;
}
// График
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 = `<table class="data-table">
<thead>
<tr>
<th>Время</th>
<th>Амплитуда</th>
<th>Сигнал</th>
</tr>
</thead>
<tbody>`;
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 += `<tr>
<td>${s.time}</td>
<td class="${ampClass}">${s.amplitude}</td>
<td>${s.signal}</td>
</tr>`;
});
html += '</tbody></table>';
DOM.dataTableContainer.innerHTML = html;
} else if (DOM.dataTableContainer) {
DOM.dataTableContainer.innerHTML = '<p>Нет данных</p>';
}
}
/**
* Отрисовка событий
*/
function renderEvents(data: EventsResponse): void {
if (!DOM.eventsContent) return;
let html = '';
if (data.events.length === 0) {
html = '<div style="color:#666;">Нет событий</div>';
} else {
data.events.forEach((e: Event) => {
html += `<div><span class="event-time">[${e.time}]</span> ${e.event}</div>`;
});
}
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 {
const orderText = data.order === 'newest_first' ? 'новые сверху' : 'старые сверху';
DOM.pageInfo.textContent = `Страница ${page} из ${total_pages} (записи ${start_index}-${end_index} из ${total}) ${orderText}`;
}
}
// Кнопки навигации
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); // Последняя страница = самые старые
}
// "◀ Предыдущая" → страница с номером на 1 меньше (более новые записи)
function goToPreviousPage(): void {
goToPage(state.currentPage - 1);
}
// "Следующая ▶" → страница с номером на 1 больше (более старые записи)
function goToNextPage(): void {
goToPage(state.currentPage + 1);
}
// ============================================
// ОБНОВЛЕНИЕ ВРЕМЕНИ
// ============================================
async function updateTime(): Promise<void> {
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 = `
<div class="pagination-controls">
<button id="firstPage" class="pagination-btn" title="Первая страница (самые новые)">⏮</button>
<button id="prevPage" class="pagination-btn" title="Предыдущая страница (более новые)">◀</button>
<span id="pageInfo" class="pagination-info">Страница 1 из 1</span>
<button id="nextPage" class="pagination-btn" title="Следующая страница (более старые)">▶</button>
<button id="lastPage" class="pagination-btn" title="Последняя страница (самые старые)">⏭</button>
<span class="pagination-goto">
<input type="number" id="pageInput" min="1" value="1" class="pagination-input">
<button id="goToPage" class="pagination-btn pagination-goto-btn">Перейти</button>
</span>
</div>
`;
// Вставляем перед содержимым событий
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) {
void loadEvents(1);
}
}
});
});
}
async function init(): Promise<void> {
console.log('[Logs] Инициализация...');
// Инициализация пагинации
initPaginationControls();
// Инициализация табов
initTabs();
// Загрузка списка файлов
await loadFileList();
// Обновление времени
await updateTime();
setInterval(() => {
void updateTime();
}, 1000);
// Если вкладка "События" активна по умолчанию, загружаем события
const eventsTab = document.getElementById('tab-events');
if (eventsTab && eventsTab.classList.contains('active')) {
await loadEvents(1);
}
console.log('[Logs] Инициализация завершена');
}
// ============================================
// ЗАПУСК
// ============================================
// Запускаем инициализацию после загрузки DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
void init();
});
} else {
void init();
}