2

Polish dashboard readability and loading state

This commit is contained in:
2026-03-12 16:17:23 +01:00
parent 87d2d5f440
commit 0bc2d2b689
2 changed files with 96 additions and 13 deletions

View File

@@ -360,7 +360,7 @@ const overviewHTML = `<!doctype html>
<style>
:root { color-scheme: dark; }
body { font-family: system-ui, sans-serif; margin: 0; background: #0f172a; color: #e2e8f0; }
header { padding: 1rem 1.5rem; border-bottom: 1px solid #334155; position: sticky; top: 0; background: rgba(15,23,42,.97); }
header { padding: 1rem 1.5rem; border-bottom: 1px solid #334155; background: #0f172a; }
main { padding: 1.5rem; display: grid; gap: 1.25rem; }
h1, h2 { margin: 0 0 .75rem 0; }
table { width: 100%; border-collapse: collapse; }
@@ -378,17 +378,20 @@ const overviewHTML = `<!doctype html>
.status.observed { background: #1e293b; }
.muted { color: #94a3b8; }
.mono { font-family: ui-monospace, monospace; }
.panel { background: #111827; border: 1px solid #334155; border-radius: .75rem; padding: 1rem; overflow: auto; }
.panel { background: #111827; border: 1px solid #334155; border-radius: .75rem; padding: 1rem; overflow: auto; -webkit-overflow-scrolling: touch; }
.controls { display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; }
.controls-group { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.leaders { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
.leader-card { background: #111827; border: 1px solid #334155; border-radius: .75rem; padding: 1rem; }
.leader-card { background: #111827; border: 1px solid #334155; border-radius: .75rem; padding: 1rem; min-height: 15rem; }
.leader-card h2 { margin-bottom: .35rem; font-size: 1rem; }
.leader-list { list-style: none; margin: .75rem 0 0 0; padding: 0; display: grid; gap: .65rem; }
.leader-item { display: grid; gap: .2rem; }
.leader-main { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
.leader-item { display: grid; gap: .2rem; padding: .55rem .65rem; border-radius: .6rem; }
.leader-item:nth-child(odd) { background: #0f172a; }
.leader-item:nth-child(even) { background: #111c31; }
.leader-main { display: flex; align-items: center; justify-content: space-between; gap: .75rem; min-width: 0; }
.leader-main .mono { overflow: hidden; text-overflow: ellipsis; }
.leader-value { font-weight: 600; white-space: nowrap; }
.leader-placeholder { color: #64748b; }
@media (max-width: 1100px) { .leaders { grid-template-columns: 1fr; } }
.toolbar { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; margin-bottom: .75rem; }
.toolbar .meta { font-size: .95rem; color: #94a3b8; }
@@ -419,6 +422,19 @@ const overviewHTML = `<!doctype html>
.bot-chip.yandex { background: #dc2626; color: white; }
.bot-chip.baidu { background: #7c3aed; color: white; }
.bot-chip.bytespider { background: #111827; color: white; }
@media (max-width: 720px) {
header { padding: .9rem 1rem; }
main { padding: 1rem; gap: 1rem; }
.panel, .leader-card, .card { padding: .85rem; }
.stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.controls { align-items: flex-start; }
.controls-group, .toolbar-right { width: 100%; justify-content: flex-start; }
.toolbar { flex-direction: column; align-items: flex-start; }
table { min-width: 720px; }
.leader-card { min-height: 0; }
.leader-item { padding: .5rem .55rem; }
.action-link, button { min-height: 2.2rem; }
}
</style>
</head>
<body>
@@ -427,7 +443,14 @@ const overviewHTML = `<!doctype html>
<div class="muted">Local-only review and enforcement console</div>
</header>
<main>
<section class="stats" id="stats"></section>
<section class="stats" id="stats">
<div class="card"><div class="muted">Total events</div><div class="stat-value">—</div></div>
<div class="card"><div class="muted">Tracked IPs</div><div class="stat-value">—</div></div>
<div class="card"><div class="muted">Blocked</div><div class="stat-value">—</div></div>
<div class="card"><div class="muted">Review</div><div class="stat-value">—</div></div>
<div class="card"><div class="muted">Allowed</div><div class="stat-value">—</div></div>
<div class="card"><div class="muted">Observed</div><div class="stat-value">—</div></div>
</section>
<section class="panel controls">
<div class="controls-group">
<label class="toggle"><input id="show-bots-toggle" type="checkbox" checked onchange="toggleKnownBots()">Show known bots</label>
@@ -435,7 +458,44 @@ const overviewHTML = `<!doctype html>
</div>
<div class="muted">These two filters affect both the leaderboards and the Recent IPs list.</div>
</section>
<section class="leaders" id="leaderboards"></section>
<section class="leaders" id="leaderboards">
<section class="leader-card">
<h2>Top IPs by events</h2>
<div class="muted">Last 24 hours</div>
<ol class="leader-list">
<li class="leader-item"><div class="leader-main"><span class="mono leader-placeholder">Loading…</span><span class="leader-value leader-placeholder">—</span></div></li>
<li class="leader-item"><div class="leader-main"><span class="mono leader-placeholder">Loading…</span><span class="leader-value leader-placeholder">—</span></div></li>
<li class="leader-item"><div class="leader-main"><span class="mono leader-placeholder">Loading…</span><span class="leader-value leader-placeholder">—</span></div></li>
</ol>
</section>
<section class="leader-card">
<h2>Top IPs by traffic</h2>
<div class="muted">Last 24 hours</div>
<ol class="leader-list">
<li class="leader-item"><div class="leader-main"><span class="mono leader-placeholder">Loading…</span><span class="leader-value leader-placeholder">—</span></div></li>
<li class="leader-item"><div class="leader-main"><span class="mono leader-placeholder">Loading…</span><span class="leader-value leader-placeholder">—</span></div></li>
<li class="leader-item"><div class="leader-main"><span class="mono leader-placeholder">Loading…</span><span class="leader-value leader-placeholder">—</span></div></li>
</ol>
</section>
<section class="leader-card">
<h2>Top sources by events</h2>
<div class="muted">Last 24 hours</div>
<ol class="leader-list">
<li class="leader-item"><div class="leader-main"><span class="mono leader-placeholder">Loading…</span><span class="leader-value leader-placeholder">—</span></div></li>
<li class="leader-item"><div class="leader-main"><span class="mono leader-placeholder">Loading…</span><span class="leader-value leader-placeholder">—</span></div></li>
<li class="leader-item"><div class="leader-main"><span class="mono leader-placeholder">Loading…</span><span class="leader-value leader-placeholder">—</span></div></li>
</ol>
</section>
<section class="leader-card">
<h2>Top URLs by events</h2>
<div class="muted">Last 24 hours</div>
<ol class="leader-list">
<li class="leader-item"><div class="leader-main"><span class="mono leader-placeholder">Loading…</span><span class="leader-value leader-placeholder">—</span></div></li>
<li class="leader-item"><div class="leader-main"><span class="mono leader-placeholder">Loading…</span><span class="leader-value leader-placeholder">—</span></div></li>
<li class="leader-item"><div class="leader-main"><span class="mono leader-placeholder">Loading…</span><span class="leader-value leader-placeholder">—</span></div></li>
</ol>
</section>
</section>
<section class="panel">
<div class="toolbar">
<h2>Recent IPs</h2>
@@ -456,7 +516,7 @@ const overviewHTML = `<!doctype html>
<th>Actions</th>
</tr>
</thead>
<tbody id="ips-body"></tbody>
<tbody id="ips-body"><tr><td colspan="7" class="muted">Loading recent IPs…</td></tr></tbody>
</table>
</section>
</main>
@@ -478,7 +538,7 @@ const overviewHTML = `<!doctype html>
reason: 'Reason',
};
const stateOrder = { blocked: 0, review: 1, observed: 2, allowed: 3 };
let currentItems = [];
let currentItems = null;
let currentSort = loadSortPreference();
let showKnownBots = loadShowKnownBotsPreference();
let showAllowed = loadShowAllowedPreference();
@@ -675,7 +735,7 @@ const overviewHTML = `<!doctype html>
return [
'<li class="leader-item">',
' <div class="leader-main">',
' <a class="mono" href="/ips/' + encodeURIComponent(item.ip) + '">' + escapeHtml(item.ip) + '</a>',
' <div class="ip-cell mono">' + renderBotChip(item.bot) + '<a href="/ips/' + encodeURIComponent(item.ip) + '">' + escapeHtml(item.ip) + '</a></div>',
' <span class="leader-value">' + escapeHtml(primaryValue) + '</span>',
' </div>',
'</li>'
@@ -811,6 +871,10 @@ const overviewHTML = `<!doctype html>
}
function renderIPs(items) {
if (!Array.isArray(items)) {
document.getElementById('ips-body').innerHTML = '<tr><td colspan="7" class="muted">Loading recent IPs…</td></tr>';
return;
}
const filteredItems = items.filter(item => (showKnownBots || !item.bot) && (showAllowed || item.state !== 'allowed') && (!showReviewOnly || item.state === 'review'));
const rows = sortItems(filteredItems).map(item => [
'<tr>',
@@ -905,9 +969,11 @@ const overviewHTML = `<!doctype html>
}
async function refresh() {
const recentResponse = await fetch('/api/recent-ips?hours=' + recentHours + '&limit=250');
const [_, recentResponse] = await Promise.all([
refreshOverview(),
fetch('/api/recent-ips?hours=' + recentHours + '&limit=250')
]);
const recentPayload = await recentResponse.json().catch(() => []);
refreshOverview();
if (!recentResponse.ok) {
const message = Array.isArray(recentPayload) ? recentResponse.statusText : (recentPayload.error || recentResponse.statusText);
document.getElementById('ips-body').innerHTML = '<tr><td colspan="7" class="muted">' + escapeHtml(message) + '</td></tr>';
@@ -933,7 +999,7 @@ const ipDetailsHTML = `<!doctype html>
<style>
:root { color-scheme: dark; }
body { font-family: system-ui, sans-serif; margin: 0; background: #020617; color: #e2e8f0; }
header { padding: 1rem 1.5rem; border-bottom: 1px solid #334155; position: sticky; top: 0; background: rgba(2,6,23,.97); }
header { padding: 1rem 1.5rem; border-bottom: 1px solid #334155; background: #020617; }
main { padding: 1.5rem; display: grid; gap: 1.5rem; }
.panel { background: #111827; border: 1px solid #334155; border-radius: .75rem; padding: 1rem; overflow: auto; }
h1, h2 { margin: 0 0 .75rem 0; }
@@ -971,6 +1037,14 @@ const ipDetailsHTML = `<!doctype html>
.mono { font-family: ui-monospace, monospace; }
a { color: #93c5fd; text-decoration: none; }
.hint { font-size: .9rem; color: #94a3b8; margin-top: .75rem; }
@media (max-width: 720px) {
header { padding: .9rem 1rem; }
main { padding: 1rem; gap: 1rem; }
.panel { padding: .85rem; -webkit-overflow-scrolling: touch; }
table { min-width: 720px; }
.actions { width: 100%; }
button { min-height: 2.2rem; }
}
</style>
</head>
<body data-ip="{{ .IP }}">