You've already forked caddy-opnsense-blocker
Polish dashboard readability and loading state
This commit is contained in:
@@ -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 }}">
|
||||
|
||||
Reference in New Issue
Block a user