Files
2026-06-06 14:52:48 +03:00

196 lines
12 KiB
HTML
Raw Permalink 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.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Analytics - LocalBooru</title>
<style>
:root { --bg-main: #1e1e1e; --bg-header: #252525; --bg-panel: #222; --bg-box: #2a2a2a; --bg-input: #333; --text-main: #eee; --text-muted: #aaa; --border-color: #444; --link-color: #4da8da; --color-g: #4caf50; --color-s: #ff9800; --color-q: #ffeb3b; --color-e: #f44336; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background-color: var(--bg-main); color: var(--text-main); font-family: sans-serif; font-size: 13px; }
header { display: flex; align-items: center; background-color: var(--bg-header); padding: 10px 20px; border-bottom: 1px solid var(--border-color); gap: 15px; flex-wrap: wrap; }
.logo { font-weight: bold; font-size: 18px; color: white; text-decoration: none; }
.analytics-link { color: var(--link-color); text-decoration: none; font-size: 12px; font-weight: bold;}
.search-wrapper { flex-grow: 1; max-width: 400px; position: relative; }
.search-wrapper input { width: 100%; background-color: var(--bg-input); border: 1px solid var(--border-color); color: white; padding: 6px 10px; border-radius: 3px; outline: none; }
.autocomplete-list { position: absolute; top: 100%; left: 0; right: 0; background-color: var(--bg-box); border: 1px solid var(--border-color); border-radius: 3px; margin-top: 4px; list-style: none; z-index: 1000; max-height: 250px; overflow-y: auto; box-shadow: 0 4px 10px rgba(0,0,0,0.5); display: none; }
.autocomplete-item { padding: 8px 10px; cursor: pointer; display: flex; justify-content: space-between; border-bottom: 1px solid #333; }
.autocomplete-item:hover { background-color: var(--bg-input); }
.autocomplete-count { color: var(--text-muted); font-size: 12px; }
main { padding: 20px; max-width: 1400px; margin: 0 auto; }
h1 { font-size: 20px; margin-bottom: 20px; color: white; }
.dashboard-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; align-items: start; }
.panel { background-color: var(--bg-panel); border: 1px solid var(--border-color); border-radius: 4px; padding: 15px; display: flex; flex-direction: column; gap: 15px; }
.panel-header { font-size: 16px; font-weight: bold; color: var(--link-color); border-bottom: 1px solid var(--border-color); padding-bottom: 10px; }
.left-col { display: flex; flex-direction: column; gap: 20px; }
.rating-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.rating-box { background-color: var(--bg-box); border: 1px solid var(--border-color); border-radius: 4px; padding: 15px; text-align: center; }
.data-list { list-style: none; display: flex; flex-direction: column; gap: 8px;}
.data-row { display: flex; justify-content: space-between; border-bottom: 1px solid #333; padding-bottom: 4px; }
.table-header { display: flex; justify-content: space-between; font-weight: bold; margin-bottom: 10px; color: white;}
.tag-row { display: flex; justify-content: space-between; margin-bottom: 8px; }
.tag-row a { color: var(--link-color); text-decoration: none; }
.tag-row a:hover { text-decoration: underline; }
.pagination { display: flex; justify-content: center; gap: 5px; margin-top: 15px; align-items: center; }
.page-btn { background: transparent; border: 1px solid var(--border-color); color: white; padding: 4px 10px; border-radius: 3px; cursor: pointer; }
.ext-block { margin-bottom: 10px; }
.ext-block a { color: var(--link-color); text-decoration: none; word-break: break-all; }
.ext-block a:hover { text-decoration: underline; }
.loader { color: var(--text-muted); font-size: 16px; }
@media (max-width: 1000px) { .dashboard-grid { grid-template-columns: 1fr 1fr; } .left-col { grid-column: 1/-1; display: grid; grid-template-columns: 1fr 1fr; } }
@media (max-width: 700px) { .dashboard-grid, .left-col { grid-template-columns: 1fr; } header { flex-wrap: wrap; } .search-wrapper { order: 3; min-width: 100%; margin-top: 10px;} }
</style>
</head>
<body>
<header>
<a href="/" class="logo">LocalBooru</a>
</header>
<main>
<h1>Database Analytics</h1>
<div id="loadingIndicator" class="loader">Fetching data from API...</div>
<div class="dashboard-grid" id="analyticsGrid" style="display: none;">
<div class="left-col">
<div class="panel">
<div class="panel-header">⭐ Rating Breakdown</div>
<div class="rating-grid" id="ratingContainer"></div>
</div>
<div class="panel">
<div class="panel-header">📁 Storage Breakdown</div>
<div class="data-list" id="storageContainer"></div>
</div>
<div class="panel">
<div class="panel-header">🎨 Media Types</div>
<div class="data-list" id="mediaContainer"></div>
</div>
<div class="panel">
<div class="panel-header">📏 Extremes (Click to view)</div>
<div id="extremesContainer"></div>
</div>
</div>
<div class="panel">
<div class="panel-header">🏷️ Top Tags List</div>
<div class="table-header"><span>Tag</span><span>Count</span></div>
<div class="data-list" id="tagsListContainer"></div>
<div class="pagination">
<button class="page-btn" onclick="prevTagPage()">&lsaquo;</button>
<span id="tagPageInfo" style="font-weight:bold; padding:0 10px;"></span>
<button class="page-btn" onclick="nextTagPage()">&rsaquo;</button>
</div>
</div>
<!-- Заменено, так как в новом JSON нет top_pairs -->
<div class="panel">
<div class="panel-header">️ System Overview</div>
<div class="data-list" id="overviewContainer"></div>
</div>
</div>
</main>
<script>
let debounceTimer;
let allTags = [], currentTagPage = 1, tagsPerPage = 22;
// Укорачиваем длинные имена файлов (хэши) для красивого отображения
const shortenName = (name) => name.length > 20 ? name.substring(0, 10) + '...' + name.substring(name.length - 6) : name;
async function fetchAnalytics() {
try {
const response = await fetch('/api', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: "analytic" })
});
if (!response.ok) throw new Error("Network response was not ok");
const data = await response.json();
// Скрываем лоадер, показываем сетку
document.getElementById('loadingIndicator').style.display = 'none';
document.getElementById('analyticsGrid').style.display = 'grid';
// 1. Рейтинги
const rStats = data.ratings_stats;
document.getElementById('ratingContainer').innerHTML = Object.keys(rStats).map(k => `
<div class="rating-box" style="border-top: 2px solid var(--color-${k[0]})">
<div style="font-weight:bold; color:var(--color-${k[0]}); margin-bottom:5px;">${k.charAt(0).toUpperCase() + k.slice(1)}</div>
<div style="color:var(--text-muted); margin-bottom:3px;">${rStats[k].count} items</div>
<div style="font-weight:bold;">${rStats[k].size_formatted}</div>
</div>
`).join('');
// 2. Хранилище (Storage)
const s = data.storage_stats.folders;
document.getElementById('storageContainer').innerHTML = `
<div class="data-row"><span>Logs:</span><span class="val">${s.logs}</span></div>
<div class="data-row"><span>DB:</span><span class="val">${s.db}</span></div>
<div class="data-row"><span>Thumb:</span><span class="val">${s.thumb}</span></div>
<div class="data-row"><span>Data (Img):</span><span class="val">${s.data}</span></div>
<div class="data-row" style="color:var(--color-g); font-weight:bold; border-top:1px solid #444; border-bottom:none; padding-top:8px;"><span>GRAND TOTAL:</span><span>${data.storage_stats.grand_total}</span></div>
`;
// 3. Медиа типы
const m = data.storage_stats.types;
document.getElementById('mediaContainer').innerHTML = `
<div class="data-row"><span>IMG:</span><span class="val">${m.images}</span></div>
<div class="data-row"><span>VID:</span><span class="val">${m.videos}</span></div>
<div class="data-row"><span>GIF:</span><span class="val">${m.gifs}</span></div>
<div style="font-size:11px; color:var(--text-muted); margin-top:5px;">Total: <b>${data.total_images_analyzed}</b> | Avg: <b>${data.average_weight.formatted}</b></div>
`;
// 4. Рекорды (Extremes)
const res = data.resolution_stats;
const weight = data.weight_stats;
document.getElementById('extremesContainer').innerHTML = `
<div class="ext-block"><b>Largest Res:</b> ${res.largest.resolution}<br>(<a href="post/${res.largest.md5}">${shortenName(res.largest.filename)}</a>)</div>
<div class="ext-block"><b>Smallest Res:</b> ${res.smallest.resolution}<br>(<a href="post/${res.smallest.md5}">${shortenName(res.smallest.filename)}</a>)</div>
<div class="ext-block"><b>Heaviest:</b> ${weight.largest.size_formatted}<br>(<a href="post/${weight.largest.md5}">${shortenName(weight.largest.filename)}</a>)</div>
<div class="ext-block"><b>Lightest:</b> ${weight.smallest.size_formatted}<br>(<a href="post/${weight.smallest.md5}">${shortenName(weight.smallest.filename)}</a>)</div>
`;
// 5. System Overview (вместо Top Pairs)
document.getElementById('overviewContainer').innerHTML = `
<div class="data-row"><span>Images Analyzed:</span><span class="val" style="font-weight:bold;">${data.total_images_analyzed}</span></div>
<div class="data-row"><span>Files Not Found:</span><span class="val" style="font-weight:bold; color: ${data.files_not_found > 0 ? 'var(--color-e)' : 'inherit'};">${data.files_not_found}</span></div>
`;
// 6. Теги
allTags = data.top_tag;
renderTagsPage();
} catch (error) {
console.error('Error fetching analytics:', error);
document.getElementById('loadingIndicator').innerHTML = `<span style="color: var(--color-e)">Failed to load data. See console for details.</span>`;
}
}
// Пагинация тегов
function renderTagsPage() {
if (!allTags || allTags.length === 0) return;
const totalPages = Math.ceil(allTags.length / tagsPerPage) || 1;
document.getElementById('tagPageInfo').innerText = `${currentTagPage} / ${totalPages}`;
const start = (currentTagPage - 1) * tagsPerPage;
document.getElementById('tagsListContainer').innerHTML = allTags.slice(start, start + tagsPerPage).map(t =>
`<div class="tag-row"><a href="index.html?query=${encodeURIComponent(t.tag)}">${t.tag}</a> <span>${t.count}</span></div>`
).join('');
}
window.prevTagPage = () => { if(currentTagPage > 1) { currentTagPage--; renderTagsPage(); }};
window.nextTagPage = () => { if(currentTagPage < Math.ceil(allTags.length/tagsPerPage)) { currentTagPage++; renderTagsPage(); }};
// Запуск
fetchAnalytics();
</script>
</body>
</html>