Compare commits

...

4 Commits

6 changed files with 568 additions and 155 deletions

View File

@@ -219,95 +219,184 @@ 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)
writeJSON(w, map[string]any{
"filename": filename,
"total": len(samples),
"samples": samples,
})
if total == 0 {
writeJSON(w, map[string]any{
"filename": filename,
"total": 0,
"page": 1,
"page_size": pageSize,
"total_pages": 1,
"has_previous": false,
"has_next": false,
"start_index": 0,
"end_index": 0,
"samples": []Sample{},
"stats": map[string]any{
"total_points": 0,
"amplitude_over_0": 0,
"signal_over_10": 0,
},
"order": "newest_first",
})
return
}
totalPages := (total + pageSize - 1) / pageSize
if totalPages == 0 {
totalPages = 1
}
// Корректируем номер страницы
if page > totalPages {
page = totalPages
}
if page < 1 {
page = 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) {

View File

@@ -545,4 +545,105 @@
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;
}
/* ===== ПУСТОЕ СОСТОЯНИЕ ===== */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #88ffbb;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-message {
font-size: var(--font-size-xl);
font-family: var(--font-display);
letter-spacing: 0.05em;
margin-bottom: 8px;
color: #00ff88;
}
.empty-hint {
font-size: var(--font-size-base);
font-family: var(--font-mono);
opacity: 0.5;
color: #88ffbb;
}
/* ===== ЗАГРУЗКА ===== */
.loading-indicator {
text-align: center;
padding: 60px 20px;
color: #88ffbb;
font-family: var(--font-mono);
font-size: var(--font-size-lg);
opacity: 0.7;
}
.loading-indicator::after {
content: '';
display: inline-block;
width: 20px;
height: 20px;
margin-left: 12px;
border: 2px solid #00ff88;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
vertical-align: middle;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ===== БОКОВАЯ ПАНЕЛЬ ===== */
.logs-sidebar {
flex: 0 0 280px;
border-right: 1px solid #00ff88;
padding-right: 16px;
padding-left: 4px;
overflow-y: auto;
overflow-x: visible;
transition: all 0.3s ease;
}
.logs-sidebar.hidden {
flex: 0 0 0 !important;
width: 0 !important;
min-width: 0 !important;
padding: 0 !important;
border: none !important;
overflow: hidden !important;
opacity: 0;
}
.logs-container {
display: flex;
gap: 20px;
height: calc(100vh - 120px);
overflow: visible;
transition: gap 0.3s ease;
}

View File

@@ -7,7 +7,7 @@
<meta name="theme-color" content="#050805">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>Грид Панель</title>
<title>G-Port Панель</title>
<link rel="icon" type="image/png" href="/favicon.png">
<!-- Локальные шрифты -->
@@ -27,7 +27,7 @@
<body>
<div class="header-bar">
<h1>ЗВЕЗДНЫЙ ГРИД</h1>
<h1>G-Port Warden</h1>
<div class="header-controls">
<a href="/logs.html" class="header-link">Журнал</a>
<div id="server-time" class="header-time">--:--:--</div>
@@ -42,13 +42,13 @@
<div class="grid">
<div class="card wide">
<h3>📊 СОСТОЯНИЕ</h3>
<h3>СОСТОЯНИЕ</h3>
<div id="status">...</div>
<div id="meta"></div>
</div>
<div class="card wide">
<h3>📈 УРОВНИ КАНАЛОВ</h3>
<h3>КАНАЛ</h3>
<div class="progress-row">
<div class="progress-label">
@@ -98,7 +98,7 @@
<div class="panel right hidden" id="rightPanel">
<div class="cam-modal">
<div class="cam-header">
<span>📹 Видео с камеры</span>
<span>Видео с камеры</span>
<button id="camCloseBtn" class="cam-close-btn" aria-label="Закрыть камеру"></button>
</div>
<div class="cam-frame-wrapper">
@@ -118,7 +118,7 @@
<!-- Подсказка по горячим клавишам -->
<div class="hotkey-hint">
<kbd>C</kbd> — камера &nbsp;|&nbsp; <kbd>F</kbd> — полный экран &nbsp;|&nbsp; <kbd>M</kbd> — звук &nbsp;|&nbsp; <kbd>Esc</kbd>сброс
(ENG lang key) <kbd>C</kbd> — камера &nbsp;|&nbsp; <kbd>M</kbd> — звук &nbsp;|&nbsp; <kbd>Esc</kbd>сброс
</div>
<script type="module" src="/dist/app.js"></script>

View File

@@ -41,7 +41,7 @@ export function initCamera(): void {
if (textSpan) {
textSpan.textContent = 'Скрыть';
} else {
btn.innerHTML = '📷 Скрыть';
btn.innerHTML = 'Скрыть';
}
}
@@ -74,7 +74,7 @@ export function initCamera(): void {
if (textSpan) {
textSpan.textContent = 'Камера';
} else {
btn.innerHTML = '📷 Камера';
btn.innerHTML = 'Камера';
}
}

View File

@@ -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,20 @@ const DOM = {
eventsContent: document.getElementById('eventsContent') as HTMLElement | null,
serverTime: document.getElementById('server-time') as HTMLElement | null,
// Элементы пагинации
// Боковая панель
logsSidebar: document.querySelector('.logs-sidebar') as HTMLElement | null,
logsContainer: document.querySelector('.logs-container') 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,
@@ -99,6 +131,29 @@ const DOM = {
goToPageBtn: document.getElementById('goToPage') as HTMLButtonElement | null,
};
// ============================================
// УПРАВЛЕНИЕ БОКОВОЙ ПАНЕЛЬЮ
// ============================================
/**
* Показать/скрыть боковую панель в зависимости от активной вкладки
*/
function toggleSidebar(show: boolean): void {
if (!DOM.logsSidebar || !DOM.logsContainer) return;
if (show) {
// Показываем панель
DOM.logsSidebar.style.display = 'block';
DOM.logsSidebar.style.flex = '0 0 280px';
DOM.logsContainer.style.gap = '20px';
} else {
// Скрываем панель
DOM.logsSidebar.style.display = 'none';
DOM.logsSidebar.style.flex = '0 0 0';
DOM.logsContainer.style.gap = '0';
}
}
// ============================================
// ФУНКЦИИ ЗАГРУЗКИ ДАННЫХ
// ============================================
@@ -125,14 +180,16 @@ async function loadFileList(): Promise<void> {
<div class="file-info">${file.time} | ${(file.size / 1024).toFixed(1)} KB</div>
`;
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 +197,20 @@ async function loadFileList(): Promise<void> {
}
/**
* Загрузка данных из файла
* Загрузка данных из файла с пагинацией
*/
async function loadFileData(filename: string): Promise<void> {
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 для преобразования 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 +221,49 @@ async function loadFileData(filename: string): Promise<void> {
}
try {
const response = await fetch(`/api/log/data?file=${encodeURIComponent(filename)}`);
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;
}
}
@@ -183,33 +277,37 @@ async function loadEvents(page: number = 1): Promise<void> {
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} (новые сверху)`);
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}`);
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>';
DOM.eventsContent.innerHTML = `
<div class="empty-state">
<div class="empty-message">Нет событий</div>
<div class="empty-hint">Журнал событий пуст</div>
</div>
`;
}
} finally {
state.isLoading = false;
@@ -227,34 +325,26 @@ 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 = `
<div class="log-stat-card">
<div class="value">${totalPoints}</div>
<div class="value">${stats.total_points}</div>
<div class="label">Всего точек</div>
</div>
<div class="log-stat-card">
<div class="value">${amplitudeOver0Count}</div>
<div class="value">${stats.amplitude_over_0}</div>
<div class="label">Амплитуда > 0</div>
</div>
<div class="log-stat-card">
<div class="value">${signalOver10Count}</div>
<div class="value">${stats.signal_over_10}</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) {
if (DOM.logChart && data.total > 0) {
const history = samples.map((s: LogSample) => s.signal);
drawChart(DOM.logChart, history);
}
@@ -264,6 +354,7 @@ function renderData(data: LogDataResponse): void {
let html = `<table class="data-table">
<thead>
<tr>
<th>#</th>
<th>Время</th>
<th>Амплитуда</th>
<th>Сигнал</th>
@@ -271,15 +362,17 @@ function renderData(data: LogDataResponse): void {
</thead>
<tbody>`;
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 += `<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>
@@ -289,7 +382,12 @@ function renderData(data: LogDataResponse): void {
html += '</tbody></table>';
DOM.dataTableContainer.innerHTML = html;
} else if (DOM.dataTableContainer) {
DOM.dataTableContainer.innerHTML = '<p>Нет данных</p>';
DOM.dataTableContainer.innerHTML = `
<div class="empty-state">
<div class="empty-message">Нет данных</div>
<div class="empty-hint">Выберите файл с данными</div>
</div>
`;
}
}
@@ -301,70 +399,121 @@ function renderEvents(data: EventsResponse): void {
let html = '';
if (data.events.length === 0) {
html = '<div style="color:#666;">Нет событий</div>';
html = `
<div class="empty-state">
<div class="empty-message">Нет событий</div>
<div class="empty-hint">Журнал событий пуст</div>
</div>
`;
} else {
data.events.forEach((e: Event) => {
html += `<div><span class="event-time">[${e.time}]</span> ${e.event}</div>`;
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 {
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 +530,6 @@ async function updateTime(): Promise<void> {
DOM.serverTime.textContent = data.server_time || new Date().toLocaleTimeString();
}
} catch (error) {
// Если сервер недоступен, используем локальное время
if (DOM.serverTime) {
DOM.serverTime.textContent = new Date().toLocaleTimeString();
}
@@ -392,16 +540,79 @@ async function updateTime(): Promise<void> {
// ИНИЦИАЛИЗАЦИЯ
// ============================================
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 = `
<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('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 = `
<div class="pagination-controls">
@@ -417,14 +628,9 @@ function initPaginationControls(): void {
</div>
`;
// Вставляем перед содержимым событий
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 +639,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 +668,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,8 +678,24 @@ function initTabs(): void {
const targetTab = document.getElementById(`tab-${tabName}`);
if (targetTab) targetTab.classList.add('active');
if (tabName === 'events') {
// Загружаем первую страницу событий
// ===== УПРАВЛЕНИЕ БОКОВОЙ ПАНЕЛЬЮ =====
if (tabName === 'data') {
// Показываем боковую панель для вкладки "Данные"
toggleSidebar(true);
// Если данные ещё не загружены, загружаем
if (!state.currentData) {
const firstFile = document.querySelector('.log-file-item');
if (firstFile) {
const fileName = firstFile.querySelector('.file-name')?.textContent;
if (fileName) {
void loadFileData(fileName, 1);
}
}
}
} else if (tabName === 'events') {
// Скрываем боковую панель для вкладки "События"
toggleSidebar(false);
// Загружаем события, если их нет
if (state.currentEvents.length === 0) {
void loadEvents(1);
}
@@ -487,25 +707,29 @@ function initTabs(): void {
async function init(): Promise<void> {
console.log('[Logs] Инициализация...');
// Инициализация пагинации
initPaginationControls();
// По умолчанию показываем боковую панель
toggleSidebar(true);
// Инициализация табов
initDataPaginationControls();
initEventsPaginationControls();
initTabs();
// Загрузка списка файлов
await loadFileList();
// Обновление времени
await updateTime();
setInterval(() => {
void updateTime();
}, 1000);
// Если вкладка "События" активна по умолчанию, загружаем события
// Проверяем, какая вкладка активна при загрузке
const dataTab = document.getElementById('tab-data');
const eventsTab = document.getElementById('tab-events');
if (eventsTab && eventsTab.classList.contains('active')) {
toggleSidebar(false);
await loadEvents(1);
} else {
toggleSidebar(true);
}
console.log('[Logs] Инициализация завершена');
@@ -515,7 +739,6 @@ async function init(): Promise<void> {
// ЗАПУСК
// ============================================
// Запускаем инициализацию после загрузки DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
void init();

View File

@@ -1,4 +1,3 @@
<!-- web/logs.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
@@ -38,7 +37,7 @@
<div class="logs-container">
<!-- Боковая панель с файлами -->
<div class="logs-sidebar">
<div class="logs-sidebar" id="logsSidebar">
<h3>Файлы данных</h3>
<div id="fileList"></div>
</div>
@@ -57,12 +56,13 @@
<canvas id="logChart" height="200"></canvas>
</div>
<div id="dataTableContainer"></div>
<!-- Пагинация для данных добавляется динамически -->
</div>
<!-- Вкладка: События -->
<div id="tab-events" class="tab-content">
<div id="eventsContent" class="events-list">Загрузка событий...</div>
<!-- Пагинация будет добавлена динамически через JS -->
<!-- Пагинация для событий добавляется динамически -->
</div>
</div>
</div>