<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<!-- 🆕 Отключаем кэш -->
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>📚 Архив логов</title>
<style>
* { box-sizing: border-box; }
body { font-family: Arial, sans-serif; padding: 12px; background: #f8f9fa; margin: 0; }
h1 { color: #333; font-size: 1.4em; margin: 8px 0; }
h2 { color: #333; font-size: 1.05em; margin: 15px 0 5px; }
.info { background: #e7f3ff; padding: 8px 12px; border-radius: 5px; margin: 8px 0; font-size: 0.88em; border-left: 4px solid #007BFF; }
.warn { background: #fff3cd; padding: 8px 12px; border-radius: 5px; margin: 8px 0; font-size: 0.88em; color: #856404; border-left: 4px solid #ffc107; }
.error { background: #f8d7da; padding: 8px 12px; border-radius: 5px; margin: 8px 0; font-size: 0.88em; color: #721c24; border-left: 4px solid #dc3545; }
.debug { background: #f1f1f1; padding: 8px; border-radius: 5px; margin: 8px 0; font-family: monospace; font-size: 0.8em; max-height: 200px; overflow: auto; white-space: pre-wrap; word-break: break-all; }
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 6px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin: 8px 0; }
th { background: #007BFF; color: white; padding: 7px 4px; text-align: center; font-size: 0.85em; }
td { padding: 7px 4px; text-align: center; border-bottom: 1px solid #eee; font-size: 0.85em; }
tr:hover { background: #f5f5f5; }
.btn { padding: 5px 10px; color: white; border: none; border-radius: 3px; cursor: pointer; text-decoration: none; display: inline-block; margin: 1px; font-size: 0.8em; }
.btn-view { background: #17a2b8; }
.btn-dl { background: #28a745; }
.btn-del { background: #dc3545; }
.btn-back { background: #6c757d; }
.btn-info { background: #17a2b8; }
.btn-page { background: #007BFF; min-width: 32px; }
.btn-page.active { background: #0056b3; font-weight: bold; }
.btn-page:disabled { background: #ccc; cursor: not-allowed; }
.section-title { padding: 8px 12px; border-radius: 5px; margin: 15px 0 8px; font-weight: bold; color: white; }
.section-5min { background: #28a745; }
.section-30min { background: #fd7e14; }
.section-60min { background: #6f42c1; }
.file-row { cursor: pointer; }
.file-row:hover { background: #e7f3ff !important; }
.file-row.selected { background: #d4edda !important; }
.pagination { display: flex; justify-content: center; align-items: center; gap: 4px; margin: 12px 0; flex-wrap: wrap; }
.loader { text-align: center; padding: 30px; color: #666; }
.spinner { border: 3px solid #f3f3f3; border-top: 3px solid #007BFF; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 10px auto; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.viewer-header { background: #e7f3ff; padding: 10px; border-radius: 5px; margin: 10px 0; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; }
.viewer-title { font-weight: bold; color: #007BFF; font-size: 1em; }
.viewer-info { font-size: 0.85em; color: #555; }
@media (max-width: 600px) {
th, td { font-size: 0.72em; padding: 5px 2px; }
.btn { font-size: 0.7em; padding: 4px 7px; }
.viewer-header { flex-direction: column; align-items: flex-start; }
}
</style>
</head>
<body>
<h1>📚 Архив логов</h1>
<div id="status-area"></div>
<div id="files-section">
<h2>📂 Доступные логи</h2>
<div id="files-list">
<div class="loader"><div class="spinner"></div><div>Загрузка списка...</div></div>
</div>
</div>
<div id="viewer-section" style="display:none;">
<div class="viewer-header">
<div>
<div class="viewer-title" id="viewer-title">—</div>
<div class="viewer-info" id="viewer-info">—</div>
</div>
<div>
<a id="viewer-dl" href="#" class="btn btn-dl">⬇ Скачать</a>
<button class="btn btn-back" onclick="closeViewer()">✕ Закрыть</button>
</div>
</div>
<div id="viewer-content"></div>
<div id="viewer-pagination" class="pagination"></div>
<!-- 🆕 Блок для отладки — показывается только при ошибке -->
<div id="debug-area" style="display:none;"></div>
</div>
<div style="margin-top:15px;">
<a href="/" class="btn btn-back">← На главную</a>
<a href="/charts" class="btn btn-info">📊 Графики</a>
<a href="/files" class="btn btn-info">📁 Файлы</a>
<a href="/info" class="btn btn-info">🔧 Диагностика</a>
</div>
<script>
let currentFile = null;
let currentPage = 1;
const PAGE_SIZE = 50;
function fmtSize(bytes) {
if (bytes < 1024) return bytes + ' Б';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' КБ';
return (bytes / 1024 / 1024).toFixed(2) + ' МБ';
}
function monthName(m) {
const names = {
'01': 'Январь', '02': 'Февраль', '03': 'Март', '04': 'Апрель',
'05': 'Май', '06': 'Июнь', '07': 'Июль', '08': 'Август',
'09': 'Сентябрь', '10': 'Октябрь', '11': 'Ноябрь', '12': 'Декабрь'
};
const parts = m.split('_');
return (names[parts[0]] || parts[0]) + ' ' + parts[1];
}
// 🆕 Универсальная функция fetch с отключением кэша и отладкой
async function fetchJson(url) {
const r = await fetch(url, {
cache: 'no-store',
headers: { 'Cache-Control': 'no-cache' }
});
const text = await r.text();
try {
return { ok: true, data: JSON.parse(text) };
} catch (e) {
// 🆕 Возвращаем сырой текст для отладки
return { ok: false, error: e.message, raw: text, status: r.status };
}
}
async function loadFiles() {
try {
const result = await fetchJson('/archive_list?t=' + Date.now());
if (!result.ok) {
document.getElementById('status-area').innerHTML =
`<div class="error">❌ Ошибка загрузки списка: ${result.error}</div>` +
`<div class="debug">Ответ сервера (${result.raw.length} символов):\n${result.raw.substring(0, 500)}</div>`;
return;
}
const data = result.data;
if (!data.sdAvailable) {
document.getElementById('status-area').innerHTML = '<div class="warn">❌ SD-карта не найдена</div>';
document.getElementById('files-list').innerHTML = '';
return;
}
if (!data.files || data.files.length === 0) {
document.getElementById('status-area').innerHTML =
'<div class="info">📦 Общий размер логов: 0 Б. Файлов пока нет.</div>';
document.getElementById('files-list').innerHTML =
'<div class="info">Логи ещё не накапливались.</div>';
return;
}
document.getElementById('status-area').innerHTML =
`<div class="info">📦 Всего файлов: <b>${data.files.length}</b> | Общий размер: <b>${fmtSize(data.totalSize)}</b></div>`;
const groups = { '5_min': [], '30_min': [], '60_min': [] };
data.files.forEach(f => { if (groups[f.type]) groups[f.type].push(f); });
Object.keys(groups).forEach(k => groups[k].sort((a, b) => b.month.localeCompare(a.month)));
let html = '';
const titles = {
'5_min': { name: '📊 Логи каждые 5 минут', cls: 'section-5min' },
'30_min': { name: '📊 Логи каждые 30 минут', cls: 'section-30min' },
'60_min': { name: '📊 Логи каждые 60 минут', cls: 'section-60min' }
};
Object.keys(groups).forEach(type => {
const files = groups[type];
if (files.length === 0) return;
const t = titles[type];
html += `<div class="section-title ${t.cls}">${t.name} (${files.length})</div>`;
html += '<table><tr><th>Месяц</th><th>Записей</th><th>Размер</th><th>Действия</th></tr>';
files.forEach(f => {
html += `<tr class="file-row" data-file="${f.name}">`;
html += `<td><b>${monthName(f.month)}</b></td>`;
html += `<td>${f.records}</td>`;
html += `<td>${fmtSize(f.size)}</td>`;
html += `<td>`;
html += `<button class="btn btn-view" onclick="openFile('${f.name}');event.stopPropagation();">👁 Просмотр</button> `;
html += `<a href="/download?file=${encodeURIComponent('/' + f.name)}" class="btn btn-dl">⬇</a> `;
html += `<button class="btn btn-del" onclick="deleteFile('${f.name}');event.stopPropagation();">🗑</button>`;
html += `</td></tr>`;
});
html += '</table>';
});
document.getElementById('files-list').innerHTML = html;
document.querySelectorAll('.file-row').forEach(row => {
row.addEventListener('click', () => openFile(row.dataset.file));
});
} catch (e) {
document.getElementById('status-area').innerHTML =
`<div class="error">❌ Ошибка: ${e.message}</div>`;
}
}
async function openFile(filename) {
currentFile = filename;
currentPage = 1;
document.getElementById('viewer-section').style.display = 'block';
document.getElementById('viewer-title').textContent = '📄 ' + filename;
document.getElementById('viewer-dl').href = '/download?file=' + encodeURIComponent('/' + filename);
document.getElementById('debug-area').style.display = 'none';
document.querySelectorAll('.file-row').forEach(r => r.classList.remove('selected'));
const row = document.querySelector(`.file-row[data-file="${filename}"]`);
if (row) row.classList.add('selected');
await loadPage();
document.getElementById('viewer-section').scrollIntoView({ behavior: 'smooth' });
}
function closeViewer() {
document.getElementById('viewer-section').style.display = 'none';
document.querySelectorAll('.file-row').forEach(r => r.classList.remove('selected'));
currentFile = null;
}
async function loadPage() {
if (!currentFile) return;
document.getElementById('viewer-content').innerHTML =
'<div class="loader"><div class="spinner"></div><div>Загрузка данных...</div></div>';
document.getElementById('viewer-pagination').innerHTML = '';
document.getElementById('debug-area').style.display = 'none';
const url = `/archive_view?file=${encodeURIComponent(currentFile)}&page=${currentPage}&size=${PAGE_SIZE}&t=${Date.now()}`;
const result = await fetchJson(url);
if (!result.ok) {
// 🆕 Показываем сырой ответ для отладки
document.getElementById('viewer-content').innerHTML =
`<div class="error">❌ Ошибка парсинга JSON: ${result.error}<br>HTTP статус: ${result.status}</div>`;
document.getElementById('debug-area').style.display = 'block';
document.getElementById('debug-area').innerHTML =
`<b>🔍 Сырой ответ сервера (${result.raw.length} символов):</b><br>` +
`<div class="debug">${escapeHtml(result.raw)}</div>`;
return;
}
const data = result.data;
if (data.error) {
document.getElementById('viewer-content').innerHTML =
`<div class="error">❌ ${data.error}</div>`;
return;
}
document.getElementById('viewer-info').innerHTML =
`Записей: <b>${data.totalRecords}</b> | Страница <b>${data.page}</b> из <b>${data.totalPages}</b>`;
let html = '<table><tr><th>🕒 Время</th><th>🌡 °C</th><th>💧 %</th><th>📊 мм рт.ст.</th></tr>';
if (!data.records || data.records.length === 0) {
html += '<tr><td colspan="4" style="text-align:center;color:#999;padding:15px">Нет данных</td></tr>';
} else {
data.records.forEach(rec => {
html += '<tr>';
html += `<td style="font-weight:bold;color:#555">${rec.time}</td>`;
html += `<td style="color:#dc3545">${Number(rec.temp).toFixed(1)}</td>`;
html += `<td style="color:#17a2b8">${Number(rec.hum).toFixed(1)}</td>`;
html += `<td style="color:#6f42c1">${Number(rec.press).toFixed(1)}</td>`;
html += '</tr>';
});
}
html += '</table>';
document.getElementById('viewer-content').innerHTML = html;
renderPagination(data.page, data.totalPages);
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
function renderPagination(current, total) {
if (total <= 1) {
document.getElementById('viewer-pagination').innerHTML = '';
return;
}
let html = '';
html += `<button class="btn btn-page" ${current === 1 ? 'disabled' : ''} onclick="goToPage(1)">«</button>`;
html += `<button class="btn btn-page" ${current === 1 ? 'disabled' : ''} onclick="goToPage(${current - 1})">‹</button>`;
const range = 2;
let start = Math.max(1, current - range);
let end = Math.min(total, current + range);
if (start > 1) html += '<span style="padding:0 4px">...</span>';
for (let i = start; i <= end; i++) {
html += `<button class="btn btn-page ${i === current ? 'active' : ''}" onclick="goToPage(${i})">${i}</button>`;
}
if (end < total) html += '<span style="padding:0 4px">...</span>';
html += `<button class="btn btn-page" ${current === total ? 'disabled' : ''} onclick="goToPage(${current + 1})">›</button>`;
html += `<button class="btn btn-page" ${current === total ? 'disabled' : ''} onclick="goToPage(${total})">»</button>`;
document.getElementById('viewer-pagination').innerHTML = html;
}
function goToPage(p) {
currentPage = p;
loadPage();
document.getElementById('viewer-section').scrollIntoView({ behavior: 'smooth' });
}
async function deleteFile(filename) {
if (!confirm(`Удалить файл ${filename}?\nЭто действие необратимо!`)) return;
try {
const r = await fetch('/archive_delete', {
method: 'POST',
cache: 'no-store',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Cache-Control': 'no-cache'
},
body: 'file=' + encodeURIComponent(filename)
});
const text = await r.text();
let data;
try { data = JSON.parse(text); }
catch (e) { throw new Error('Невалидный ответ: ' + text.substring(0, 200)); }
if (data.error) throw new Error(data.error);
if (currentFile === filename) closeViewer();
loadFiles();
} catch (e) {
alert('Ошибка удаления: ' + e.message);
}
}
loadFiles();
</script>
</body>
</html>