You've already forked caddy-opnsense-blocker
Refine dashboard leaderboard filters and layout
This commit is contained in:
@@ -17,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
type App interface {
|
||||
GetOverview(ctx context.Context, since time.Time, limit int) (model.Overview, error)
|
||||
GetOverview(ctx context.Context, since time.Time, limit int, options model.OverviewOptions) (model.Overview, error)
|
||||
ListEvents(ctx context.Context, limit int) ([]model.Event, error)
|
||||
ListIPs(ctx context.Context, limit int, state string) ([]model.IPState, error)
|
||||
ListRecentIPs(ctx context.Context, since time.Time, limit int) ([]model.RecentIPRow, error)
|
||||
@@ -107,7 +107,11 @@ func (h *handler) handleAPIOverview(w http.ResponseWriter, r *http.Request) {
|
||||
hours = 24
|
||||
}
|
||||
since := time.Now().UTC().Add(-time.Duration(hours) * time.Hour)
|
||||
overview, err := h.app.GetOverview(r.Context(), since, limit)
|
||||
options := model.OverviewOptions{
|
||||
ShowKnownBots: queryBool(r, "show_known_bots", true),
|
||||
ShowAllowed: queryBool(r, "show_allowed", true),
|
||||
}
|
||||
overview, err := h.app.GetOverview(r.Context(), since, limit, options)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
@@ -277,6 +281,21 @@ func queryInt(r *http.Request, name string, fallback int) int {
|
||||
return parsed
|
||||
}
|
||||
|
||||
func queryBool(r *http.Request, name string, fallback bool) bool {
|
||||
value := strings.TrimSpace(strings.ToLower(r.URL.Query().Get(name)))
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
switch value {
|
||||
case "1", "true", "yes", "on":
|
||||
return true
|
||||
case "0", "false", "no", "off":
|
||||
return false
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
@@ -360,7 +379,9 @@ const overviewHTML = `<!doctype html>
|
||||
.muted { color: #94a3b8; }
|
||||
.mono { font-family: ui-monospace, monospace; }
|
||||
.panel { background: #111827; border: 1px solid #334155; border-radius: .75rem; padding: 1rem; overflow: auto; }
|
||||
.leaders { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1rem; }
|
||||
.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 h2 { margin-bottom: .35rem; font-size: 1rem; }
|
||||
.leader-list { list-style: none; margin: .75rem 0 0 0; padding: 0; display: grid; gap: .65rem; }
|
||||
@@ -368,7 +389,7 @@ const overviewHTML = `<!doctype html>
|
||||
.leader-main { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
|
||||
.leader-main .mono { overflow: hidden; text-overflow: ellipsis; }
|
||||
.leader-value { font-weight: 600; white-space: nowrap; }
|
||||
.leader-sub { font-size: .87rem; color: #94a3b8; }
|
||||
@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; }
|
||||
.toolbar-right { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; justify-content: flex-end; }
|
||||
@@ -407,13 +428,18 @@ const overviewHTML = `<!doctype html>
|
||||
</header>
|
||||
<main>
|
||||
<section class="stats" id="stats"></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>
|
||||
<label class="toggle"><input id="show-allowed-toggle" type="checkbox" checked onchange="toggleAllowed()">Show allowed</label>
|
||||
</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="panel">
|
||||
<div class="toolbar">
|
||||
<h2>Recent IPs</h2>
|
||||
<div class="toolbar-right">
|
||||
<label class="toggle"><input id="show-bots-toggle" type="checkbox" checked onchange="toggleKnownBots()">Show known bots</label>
|
||||
<label class="toggle"><input id="show-allowed-toggle" type="checkbox" checked onchange="toggleAllowed()">Show allowed</label>
|
||||
<label class="toggle"><input id="show-review-toggle" type="checkbox" onchange="toggleReviewOnly()">Review only</label>
|
||||
<div class="meta">Last 24 hours · click a column to sort</div>
|
||||
</div>
|
||||
@@ -638,24 +664,20 @@ const overviewHTML = `<!doctype html>
|
||||
}
|
||||
|
||||
function renderTopIPs(items, primaryMetric) {
|
||||
const filteredItems = (Array.isArray(items) ? items : []).filter(item => showKnownBots || !item.bot);
|
||||
if (filteredItems.length === 0) {
|
||||
const visibleItems = Array.isArray(items) ? items : [];
|
||||
if (visibleItems.length === 0) {
|
||||
return '<div class="muted">No matching IP activity in the selected window.</div>';
|
||||
}
|
||||
return '<ol class="leader-list">' + filteredItems.map(item => {
|
||||
return '<ol class="leader-list">' + visibleItems.map(item => {
|
||||
const primaryValue = primaryMetric === 'traffic'
|
||||
? formatBytes(item.traffic_bytes)
|
||||
: String(item.events || 0) + ' event' + (Number(item.events || 0) === 1 ? '' : 's');
|
||||
const secondaryValue = primaryMetric === 'traffic'
|
||||
? String(item.events || 0) + ' event' + (Number(item.events || 0) === 1 ? '' : 's')
|
||||
: formatBytes(item.traffic_bytes);
|
||||
: String(item.events || 0);
|
||||
return [
|
||||
'<li class="leader-item">',
|
||||
' <div class="leader-main">',
|
||||
' <div class="ip-cell mono">' + renderBotChip(item.bot) + '<a href="/ips/' + encodeURIComponent(item.ip) + '">' + escapeHtml(item.ip) + '</a></div>',
|
||||
' <a class="mono" href="/ips/' + encodeURIComponent(item.ip) + '">' + escapeHtml(item.ip) + '</a>',
|
||||
' <span class="leader-value">' + escapeHtml(primaryValue) + '</span>',
|
||||
' </div>',
|
||||
' <div class="leader-sub">' + escapeHtml(secondaryValue) + ' · last seen ' + escapeHtml(formatDate(item.last_seen_at)) + '</div>',
|
||||
'</li>'
|
||||
].join('');
|
||||
}).join('') + '</ol>';
|
||||
@@ -669,9 +691,8 @@ const overviewHTML = `<!doctype html>
|
||||
'<li class="leader-item">',
|
||||
' <div class="leader-main">',
|
||||
' <span class="mono">' + escapeHtml(item.source_name || '—') + '</span>',
|
||||
' <span class="leader-value">' + escapeHtml(String(item.events || 0) + ' event' + (Number(item.events || 0) === 1 ? '' : 's')) + '</span>',
|
||||
' <span class="leader-value">' + escapeHtml(String(item.events || 0)) + '</span>',
|
||||
' </div>',
|
||||
' <div class="leader-sub">' + escapeHtml(formatBytes(item.traffic_bytes)) + ' · last seen ' + escapeHtml(formatDate(item.last_seen_at)) + '</div>',
|
||||
'</li>'
|
||||
].join('')).join('') + '</ol>';
|
||||
}
|
||||
@@ -686,9 +707,8 @@ const overviewHTML = `<!doctype html>
|
||||
'<li class="leader-item">',
|
||||
' <div class="leader-main">',
|
||||
' <span class="mono">' + escapeHtml(label) + '</span>',
|
||||
' <span class="leader-value">' + escapeHtml(String(item.events || 0) + ' event' + (Number(item.events || 0) === 1 ? '' : 's')) + '</span>',
|
||||
' <span class="leader-value">' + escapeHtml(String(item.events || 0)) + '</span>',
|
||||
' </div>',
|
||||
' <div class="leader-sub">' + escapeHtml(formatBytes(item.traffic_bytes)) + ' · last seen ' + escapeHtml(formatDate(item.last_seen_at)) + '</div>',
|
||||
'</li>'
|
||||
].join('');
|
||||
}).join('') + '</ol>';
|
||||
@@ -837,8 +857,7 @@ const overviewHTML = `<!doctype html>
|
||||
showKnownBots = !toggle || toggle.checked;
|
||||
saveShowKnownBotsPreference(showKnownBots);
|
||||
render();
|
||||
const overviewStats = window.__overviewPayload || {};
|
||||
renderLeaderboards(overviewStats);
|
||||
refreshOverview();
|
||||
}
|
||||
|
||||
function toggleAllowed() {
|
||||
@@ -846,6 +865,7 @@ const overviewHTML = `<!doctype html>
|
||||
showAllowed = !toggle || toggle.checked;
|
||||
saveShowAllowedPreference(showAllowed);
|
||||
render();
|
||||
refreshOverview();
|
||||
}
|
||||
|
||||
function toggleReviewOnly() {
|
||||
@@ -873,18 +893,21 @@ const overviewHTML = `<!doctype html>
|
||||
await refresh();
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const [overviewResponse, recentResponse] = await Promise.all([
|
||||
fetch('/api/overview?hours=' + recentHours + '&limit=10'),
|
||||
fetch('/api/recent-ips?hours=' + recentHours + '&limit=250')
|
||||
]);
|
||||
const overviewPayload = await overviewResponse.json().catch(() => ({}));
|
||||
const recentPayload = await recentResponse.json().catch(() => []);
|
||||
if (overviewResponse.ok) {
|
||||
window.__overviewPayload = overviewPayload || {};
|
||||
renderStats(overviewPayload || {});
|
||||
renderLeaderboards(overviewPayload || {});
|
||||
async function refreshOverview() {
|
||||
const response = await fetch('/api/overview?hours=' + recentHours + '&limit=10&show_known_bots=' + (showKnownBots ? 'true' : 'false') + '&show_allowed=' + (showAllowed ? 'true' : 'false'));
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
window.__overviewPayload = payload || {};
|
||||
renderStats(payload || {});
|
||||
renderLeaderboards(payload || {});
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const recentResponse = await 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>';
|
||||
|
||||
Reference in New Issue
Block a user