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

690 lines
25 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;
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,
// Элементы пагинации для данных
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,
};
// ============================================
// ФУНКЦИИ ЗАГРУЗКИ ДАННЫХ
// ============================================
/**
* Загрузка списка файлов
*/
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', () => {
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<void> {
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 = '<div class="loading-indicator">⏳ Загрузка данных...</div>';
}
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 = `
<div class="empty-state">
<div class="empty-message">Нет данных для отображения</div>
<div class="empty-hint">Файл пуст или данные ещё не поступили</div>
</div>
`;
}
// Сбрасываем пагинацию
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<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);
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 = `
<div class="empty-state">
<div class="empty-message">Нет событий</div>
<div class="empty-hint">Журнал событий пуст</div>
</div>
`;
}
} 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 = `
<div class="log-stat-card">
<div class="value">${stats.total_points}</div>
<div class="label">Всего точек</div>
</div>
<div class="log-stat-card">
<div class="value">${stats.amplitude_over_0}</div>
<div class="label">Амплитуда > 0</div>
</div>
<div class="log-stat-card">
<div class="value">${stats.signal_over_10}</div>
<div class="label">Сигнал > 10</div>
</div>
`;
}
// График - показываем все данные (ограничиваем для производительности)
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 = `<table class="data-table">
<thead>
<tr>
<th>#</th>
<th>Время</th>
<th>Амплитуда</th>
<th>Сигнал</th>
</tr>
</thead>
<tbody>`;
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 += `<tr>
<td style="opacity:0.3;font-size:var(--font-size-xs);">${num}</td>
<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, index: number) => {
const num = index + 1;
html += `<div>
<span class="event-num">#${num}</span>
<span class="event-time">[${e.time}]</span>
${e.event}
</div>`;
});
}
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<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 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 = `
<div class="pagination-controls">
<button id="dataFirstPage" class="pagination-btn" title="Первая страница (самые новые)">⏮</button>
<button id="dataPrevPage" class="pagination-btn" title="Предыдущая страница (более новые)">◀</button>
<span id="dataPageInfo" class="pagination-info">Страница 1 из 1</span>
<button id="dataNextPage" class="pagination-btn" title="Следующая страница (более старые)">▶</button>
<button id="dataLastPage" class="pagination-btn" title="Последняя страница (самые старые)">⏭</button>
<span class="pagination-goto">
<input type="number" id="dataPageInput" min="1" value="1" class="pagination-input">
<button id="dataGoToPage" class="pagination-btn pagination-goto-btn">Перейти</button>
</span>
</div>
`;
// Вставляем перед таблицей
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 = `
<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>
`;
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 === 'events' && state.currentEvents.length === 0) {
void loadEvents(1);
}
});
});
}
async function init(): Promise<void> {
console.log('[Logs] Инициализация...');
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);
}
console.log('[Logs] Инициализация завершена');
}
// ============================================
// ЗАПУСК
// ============================================
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
void init();
});
} else {
void init();
}