Доработка Пагинации в логи

This commit is contained in:
Maxim
2026-06-18 11:01:39 +03:00
parent 0cea7d5e9d
commit 35caa2bb72
4 changed files with 745 additions and 243 deletions

View File

@@ -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")

View File

@@ -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;
}
}

514
cmd/server/web/js/logs.ts Normal file
View File

@@ -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<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', () => 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}`;
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 = '<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 {
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<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) {
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();
}

View File

@@ -21,9 +21,9 @@
<body>
<div class="header-bar">
<h1>ЖУРНАЛ ЛОГОВ</h1>
<h1>📋 ЖУРНАЛ ЛОГОВ</h1>
<div id="server-time" class="header-time">--:--:--</div>
<a href="/" style="color:#00ff88; text-decoration:none; font-size:14px;">На главную</a>
<a href="/" style="color:#00ff88; text-decoration:none; font-size:14px;">В режим работы</a>
</div>
<div class="logs-container">
@@ -47,208 +47,14 @@
</div>
<div id="tab-events" class="tab-content">
<!-- Пагинация будет добавлена динамически через JS -->
<div id="eventsContent" class="events-list">Загрузка...</div>
</div>
</div>
</div>
<script type="module">
import { drawChart } from '/dist/chart.js';
// Состояние
let currentFile = null;
let currentData = null;
let currentEvents = [];
// DOM элементы
const fileList = document.getElementById('fileList');
const logStats = document.getElementById('logStats');
const dataTableContainer = document.getElementById('dataTableContainer');
const logChart = document.getElementById('logChart');
const eventsContent = document.getElementById('eventsContent');
// Загрузка списка файлов
async function loadFileList() {
try {
const response = await fetch('/api/log/files');
const files = await response.json();
fileList.innerHTML = '';
files.forEach(file => {
const div = document.createElement('div');
div.className = 'log-file-item';
if (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', () => loadFileData(file.name));
fileList.appendChild(div);
});
// Автоматически загружаем первый файл
if (files.length > 0 && !currentFile) {
loadFileData(files[0].name);
}
} catch (error) {
console.error('Error loading file list:', error);
}
}
// Загрузка данных файла
async function loadFileData(filename) {
currentFile = filename;
// Обновляем активный элемент в списке
document.querySelectorAll('.log-file-item').forEach(el => el.classList.remove('active'));
const items = document.querySelectorAll('.log-file-item');
for (const item of items) {
if (item.textContent.includes(filename)) {
item.classList.add('active');
break;
}
}
try {
const response = await fetch(`/api/log/data?file=${encodeURIComponent(filename)}`);
const data = await response.json();
currentData = data;
renderData(data);
} catch (error) {
console.error('Error loading data:', error);
}
}
// Отрисовка данных
function renderData(data) {
const samples = data.samples || [];
// Статистика
if (samples.length > 0) {
const totalPoints = samples.length;
// Амплитуда > 0 (старшие 2 бита)
const amplitudeOver0Count = samples.filter(s => s.amplitude > 0).length;
// Сигнал > 10 (младшие 6 бит)
const signalOver10Count = samples.filter(s => s.signal > 10).length;
// Общее количество объектов (сумма всех сигналов)
const totalObjects = samples.reduce((sum, s) => sum + s.signal, 0);
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 (logChart && samples.length > 0) {
const history = samples.map(s => s.signal); // Используем signal вместо count
drawChart(logChart, history);
}
// Таблица - убрали колонку "Объектов"
if (samples.length > 0) {
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 => {
// Цвет в зависимости от амплитуды
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>';
dataTableContainer.innerHTML = html;
} else {
dataTableContainer.innerHTML = '<p>Нет данных</p>';
}
}
// Загрузка событий
async function loadEvents() {
try {
const response = await fetch('/api/log/events?limit=200');
const data = await response.json();
currentEvents = data.events || [];
let html = '';
currentEvents.forEach(e => {
html += `<div><span class="event-time">[${e.time}]</span> ${e.event}</div>`;
});
eventsContent.innerHTML = html || 'Нет событий';
} catch (error) {
eventsContent.innerHTML = 'Ошибка загрузки событий';
}
}
// Переключение табов
document.querySelectorAll('.tab-btn[data-tab]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn[data-tab]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const tabName = btn.dataset.tab;
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
document.getElementById(`tab-${tabName}`).classList.add('active');
if (tabName === 'events') {
loadEvents();
}
});
});
// Инициализация
loadFileList();
// Обновление времени с сервера (единый формат)
async function updateTime() {
try {
const response = await fetch('/api/health');
const data = await response.json();
document.getElementById('server-time').textContent = data.server_time || new Date().toLocaleTimeString();
} catch (error) {
// Если сервер недоступен, используем локальное время
const now = new Date();
document.getElementById('server-time').textContent = now.toLocaleTimeString();
}
}
setInterval(updateTime, 1000);
updateTime();
</script>
<!-- Подключаем скомпилированный JS -->
<script type="module" src="/dist/logs.js"></script>
</body>
</html>