0.1.9s
This commit is contained in:
+92
-99
@@ -41,22 +41,21 @@
|
||||
.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="index.html" class="logo">LocalBooru</a>
|
||||
<a href="analytics.html" class="analytics-link">📊 Analytics</a>
|
||||
<div class="search-wrapper">
|
||||
<input type="text" id="searchInput" placeholder="Search tags...">
|
||||
<ul id="autocompleteList" class="autocomplete-list"></ul>
|
||||
</div>
|
||||
<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">
|
||||
@@ -68,7 +67,7 @@
|
||||
<div class="data-list" id="storageContainer"></div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-header">🎨 Media Types (in img/)</div>
|
||||
<div class="panel-header">🎨 Media Types</div>
|
||||
<div class="data-list" id="mediaContainer"></div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
@@ -78,125 +77,119 @@
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-header">🏷️ All Tags List</div>
|
||||
<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()">‹</button><span id="tagPageInfo" style="font-weight:bold; padding:0 10px;"></span><button class="page-btn" onclick="nextTagPage()">›</button></div>
|
||||
<div class="pagination">
|
||||
<button class="page-btn" onclick="prevTagPage()">‹</button>
|
||||
<span id="tagPageInfo" style="font-weight:bold; padding:0 10px;"></span>
|
||||
<button class="page-btn" onclick="nextTagPage()">›</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Заменено, так как в новом JSON нет top_pairs -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">🔗 Top 30 Tag Pairs</div>
|
||||
<div class="table-header"><span>Pair</span><span>Count</span></div>
|
||||
<div class="data-list" id="pairsContainer"></div>
|
||||
<div class="panel-header">ℹ️ System Overview</div>
|
||||
<div class="data-list" id="overviewContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const list = document.getElementById('autocompleteList');
|
||||
let debounceTimer;
|
||||
|
||||
searchInput.addEventListener('input', () => {
|
||||
clearTimeout(debounceTimer);
|
||||
const words = searchInput.value.split(' ');
|
||||
const currentWord = words[words.length - 1];
|
||||
if (currentWord.length < 2) { list.style.display = 'none'; return; }
|
||||
|
||||
debounceTimer = setTimeout(async () => {
|
||||
const req = { type: "autocomplete", q: currentWord };
|
||||
const data = await mockAutocompleteAPI(req);
|
||||
|
||||
if (data.length === 0) { list.style.display = 'none'; return; }
|
||||
|
||||
list.innerHTML = data.map(item => `<li class="autocomplete-item" data-tag="${item.tag}"><span>${item.tag}</span><span class="autocomplete-count">${item.count}</span></li>`).join('');
|
||||
list.style.display = 'block';
|
||||
|
||||
list.querySelectorAll('.autocomplete-item').forEach(li => {
|
||||
li.addEventListener('click', () => {
|
||||
words[words.length - 1] = li.getAttribute('data-tag');
|
||||
searchInput.value = words.join(' ') + ' ';
|
||||
list.style.display = 'none';
|
||||
searchInput.focus();
|
||||
});
|
||||
});
|
||||
}, 200);
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => { if (e.target !== searchInput && e.target !== list) list.style.display = 'none'; });
|
||||
searchInput.addEventListener('keypress', (e) => { if(e.key === 'Enter') window.location.href = `index.html?query=${searchInput.value.trim()}`; });
|
||||
|
||||
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() {
|
||||
const data = await mockAnalyticsAPI({ type: "analytics" });
|
||||
|
||||
document.getElementById('ratingContainer').innerHTML = Object.keys(data.ratings).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;">${data.ratings[k].count} items</div>
|
||||
<div style="font-weight:bold;">${data.ratings[k].size}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
try {
|
||||
const response = await fetch('/api', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: "analytic" })
|
||||
});
|
||||
|
||||
const s = data.storage;
|
||||
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>Img:</span><span class="val">${s.main}</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>${s.total}</span></div>
|
||||
`;
|
||||
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';
|
||||
|
||||
const m = data.media;
|
||||
document.getElementById('mediaContainer').innerHTML = `
|
||||
<div class="data-row"><span>IMG:</span><span class="val">${m.img_size}</span></div>
|
||||
<div class="data-row"><span>VID:</span><span class="val">${m.vid_size}</span></div>
|
||||
<div class="data-row"><span>GIF:</span><span class="val">${m.gif_size}</span></div>
|
||||
<div style="font-size:11px; color:var(--text-muted); margin-top:5px;">Total: <b>${m.total_items}</b> | Avg: <b>${m.avg_weight}</b></div>
|
||||
`;
|
||||
// 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('');
|
||||
|
||||
const e = data.extremes;
|
||||
document.getElementById('extremesContainer').innerHTML = `
|
||||
<div class="ext-block"><b>Largest:</b> ${e.largest.val}<br>(<a href="post.html?id=${e.largest.id}">${e.largest.file}</a>)</div>
|
||||
<div class="ext-block"><b>Smallest:</b> ${e.smallest.val}<br>(<a href="post.html?id=${e.smallest.id}">${e.smallest.file}</a>)</div>
|
||||
<div class="ext-block"><b>Heaviest:</b> ${e.heaviest.val}<br>(<a href="post.html?id=${e.heaviest.id}">${e.heaviest.file}</a>)</div>
|
||||
<div class="ext-block"><b>Lightest:</b> ${e.lightest.val}<br>(<a href="post.html?id=${e.lightest.id}">${e.lightest.file}</a>)</div>
|
||||
`;
|
||||
// 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>
|
||||
`;
|
||||
|
||||
document.getElementById('pairsContainer').innerHTML = data.top_pairs.map(p => `<div class="tag-row"><a href="index.html?query=${p.pair.replace(/ \+ /g, '+')}">${p.pair}</a> <span>${p.count}</span></div>`).join('');
|
||||
// 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>
|
||||
`;
|
||||
|
||||
allTags = data.all_tags; renderTagsPage();
|
||||
document.getElementById('analyticsGrid').style.display = 'grid';
|
||||
// 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() {
|
||||
document.getElementById('tagPageInfo').innerText = `${currentTagPage} / ${Math.ceil(allTags.length / tagsPerPage)}`;
|
||||
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=${t.name}">${t.name}</a> <span>${t.count}</span></div>`).join('');
|
||||
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(); }};
|
||||
|
||||
// --- ЗАГЛУШКИ ---
|
||||
function mockAutocompleteAPI(req) { return new Promise(res => { const db = [{tag: "long_hair", count: 8184}, {tag: "1girl", count: 11061}]; res(db.filter(t => t.tag.includes(req.q.toLowerCase()))); }); }
|
||||
|
||||
function mockAnalyticsAPI() {
|
||||
return new Promise(res => { setTimeout(() => { res({
|
||||
ratings: { explicit: { count: 1605, size: "4.52 GB" }, sensitive: { count: 6623, size: "19.35 GB" }, questionable: { count: 1591, size: "4.72 GB" }, general: { count: 4184, size: "10.29 GB" } },
|
||||
storage: { logs: "14.44 MB", db: "24.39 MB", thumb: "331.45 MB", main: "38.88 GB", total: "39.24 GB" },
|
||||
media: { img_size: "38.56 GB", vid_size: "140.80 MB", gif_size: "181.30 MB", total_items: 14003, avg_weight: "2.84 MB" },
|
||||
extremes: {
|
||||
largest: { val: "9921x14031", id: "5a63c", file: "5a63c28906dc7e0e...png" },
|
||||
smallest: { val: "240x280", id: "8fc61", file: "8fc614e712ae2ad...gif" },
|
||||
heaviest: { val: "65.21 MB", id: "4ab6a", file: "4ab6acf0a32c61...png" },
|
||||
lightest: { val: "6.49 KB", id: "28c67", file: "28c679ea3483e1...png" }
|
||||
},
|
||||
all_tags: Array.from({length: 100}, (_,i) => ({name:`tag_${i}`, count:100-i})),
|
||||
top_pairs: [{ pair: "1girl + highres", count: 10236 }, { pair: "1girl + solo", count: 8902 }]
|
||||
}); }, 300);});
|
||||
}
|
||||
|
||||
// Запуск
|
||||
fetchAnalytics();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -64,9 +64,7 @@
|
||||
|
||||
<header>
|
||||
<a href="/" class="logo">LocalBooru</a>
|
||||
<!--
|
||||
<a class="analytics" href="/analytics">📊 Analytics</a>
|
||||
-->
|
||||
<a class="tasks" href="/task">⚙️ Tasks</a>
|
||||
<div class="search-wrapper">
|
||||
<input type="text" id="searchInput" placeholder="Search tags...">
|
||||
|
||||
@@ -56,10 +56,6 @@
|
||||
<body>
|
||||
<header>
|
||||
<a href="/" class="logo">LocalBooru</a>
|
||||
<div class="search-wrapper">
|
||||
<input type="text" id="searchInput" placeholder="Search tags...">
|
||||
<ul id="autocompleteList" class="autocomplete-list"></ul>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
@@ -139,44 +135,6 @@
|
||||
btn.innerText = "Запустить";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === ЛОГИКА ПОИСКА (оставлена из предыдущей версии) ===
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const list = document.getElementById('autocompleteList');
|
||||
let debounceTimer;
|
||||
|
||||
searchInput.addEventListener('input', () => {
|
||||
clearTimeout(debounceTimer);
|
||||
const words = searchInput.value.split(' ');
|
||||
const currentWord = words[words.length - 1];
|
||||
if (currentWord.length < 2) { list.style.display = 'none'; return; }
|
||||
|
||||
debounceTimer = setTimeout(async () => {
|
||||
const req = { type: "autocomplete", q: currentWord };
|
||||
const data = await mockAutocompleteAPI(req);
|
||||
|
||||
if (data.length === 0) { list.style.display = 'none'; return; }
|
||||
|
||||
list.innerHTML = data.map(item => `<li class="autocomplete-item" data-tag="${item.tag}"><span>${item.tag}</span><span class="autocomplete-count">${item.count}</span></li>`).join('');
|
||||
list.style.display = 'block';
|
||||
|
||||
list.querySelectorAll('.autocomplete-item').forEach(li => {
|
||||
li.addEventListener('click', () => {
|
||||
words[words.length - 1] = li.getAttribute('data-tag');
|
||||
searchInput.value = words.join(' ') + ' ';
|
||||
list.style.display = 'none';
|
||||
searchInput.focus();
|
||||
});
|
||||
});
|
||||
}, 200);
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => { if (e.target !== searchInput && e.target !== list) list.style.display = 'none'; });
|
||||
searchInput.addEventListener('keypress', (e) => { if(e.key === 'Enter') window.location.href = `index.html?query=${searchInput.value.trim()}`; });
|
||||
|
||||
// Заглушка для поиска (оставьте или замените на ваш fetch)
|
||||
function mockAutocompleteAPI(req) { return new Promise(res => { const db = [{tag: "long_hair", count: 8184}, {tag: "1girl", count: 11061}]; res(db.filter(t => t.tag.includes(req.q.toLowerCase()))); }); }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user