You've already forked caddy-opnsense-blocker
Persist dashboard bot filter and sort
This commit is contained in:
@@ -357,6 +357,9 @@ const overviewHTML = `<!doctype html>
|
|||||||
.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; }
|
||||||
.toolbar { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; margin-bottom: .75rem; }
|
.toolbar { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; margin-bottom: .75rem; }
|
||||||
.toolbar .meta { font-size: .95rem; color: #94a3b8; }
|
.toolbar .meta { font-size: .95rem; color: #94a3b8; }
|
||||||
|
.toolbar-right { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; justify-content: flex-end; }
|
||||||
|
.toggle { display: inline-flex; align-items: center; gap: .45rem; font-size: .95rem; color: #cbd5e1; }
|
||||||
|
.toggle input { margin: 0; }
|
||||||
.sort-button { appearance: none; background: transparent; border: 0; color: inherit; cursor: pointer; font: inherit; padding: 0; }
|
.sort-button { appearance: none; background: transparent; border: 0; color: inherit; cursor: pointer; font: inherit; padding: 0; }
|
||||||
.sort-button[data-active="true"] { color: #dbeafe; }
|
.sort-button[data-active="true"] { color: #dbeafe; }
|
||||||
.actions { display: flex; gap: .35rem; flex-wrap: wrap; }
|
.actions { display: flex; gap: .35rem; flex-wrap: wrap; }
|
||||||
@@ -393,8 +396,11 @@ const overviewHTML = `<!doctype html>
|
|||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<h2>Recent IPs</h2>
|
<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>
|
||||||
<div class="meta">Last 24 hours · click a column to sort</div>
|
<div class="meta">Last 24 hours · click a column to sort</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -413,6 +419,11 @@ const overviewHTML = `<!doctype html>
|
|||||||
</main>
|
</main>
|
||||||
<script>
|
<script>
|
||||||
const recentHours = 24;
|
const recentHours = 24;
|
||||||
|
const storageKeys = {
|
||||||
|
showKnownBots: 'caddy-opnsense-blocker.overview.showKnownBots',
|
||||||
|
sortKey: 'caddy-opnsense-blocker.overview.sortKey',
|
||||||
|
sortDirection: 'caddy-opnsense-blocker.overview.sortDirection',
|
||||||
|
};
|
||||||
const sortLabels = {
|
const sortLabels = {
|
||||||
ip: 'IP',
|
ip: 'IP',
|
||||||
source: 'Source',
|
source: 'Source',
|
||||||
@@ -423,7 +434,8 @@ const overviewHTML = `<!doctype html>
|
|||||||
};
|
};
|
||||||
const stateOrder = { blocked: 0, review: 1, observed: 2, allowed: 3 };
|
const stateOrder = { blocked: 0, review: 1, observed: 2, allowed: 3 };
|
||||||
let currentItems = [];
|
let currentItems = [];
|
||||||
let currentSort = { key: 'events', direction: 'desc' };
|
let currentSort = loadSortPreference();
|
||||||
|
let showKnownBots = loadShowKnownBotsPreference();
|
||||||
|
|
||||||
function renderStats(data) {
|
function renderStats(data) {
|
||||||
const stats = [
|
const stats = [
|
||||||
@@ -446,6 +458,51 @@ const overviewHTML = `<!doctype html>
|
|||||||
return String(value ?? '').replace(/[&<>"']/g, character => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[character]));
|
return String(value ?? '').replace(/[&<>"']/g, character => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[character]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadShowKnownBotsPreference() {
|
||||||
|
try {
|
||||||
|
const rawValue = window.localStorage.getItem(storageKeys.showKnownBots);
|
||||||
|
if (rawValue === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return rawValue !== 'false';
|
||||||
|
} catch (_) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveShowKnownBotsPreference(value) {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(storageKeys.showKnownBots, value ? 'true' : 'false');
|
||||||
|
} catch (_) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSortPreference() {
|
||||||
|
const fallback = { key: 'events', direction: 'desc' };
|
||||||
|
try {
|
||||||
|
const key = window.localStorage.getItem(storageKeys.sortKey);
|
||||||
|
const direction = window.localStorage.getItem(storageKeys.sortDirection);
|
||||||
|
const validKeys = new Set(['ip', 'source', 'state', 'events', 'last_seen', 'reason']);
|
||||||
|
if (!validKeys.has(key || '')) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
if (direction !== 'asc' && direction !== 'desc') {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return { key, direction };
|
||||||
|
} catch (_) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSortPreference() {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(storageKeys.sortKey, currentSort.key);
|
||||||
|
window.localStorage.setItem(storageKeys.sortDirection, currentSort.direction);
|
||||||
|
} catch (_) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return '—';
|
return '—';
|
||||||
@@ -506,6 +563,10 @@ const overviewHTML = `<!doctype html>
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateSortButtons() {
|
function updateSortButtons() {
|
||||||
|
const toggle = document.getElementById('show-bots-toggle');
|
||||||
|
if (toggle) {
|
||||||
|
toggle.checked = showKnownBots;
|
||||||
|
}
|
||||||
document.querySelectorAll('button[data-sort]').forEach(button => {
|
document.querySelectorAll('button[data-sort]').forEach(button => {
|
||||||
const key = button.dataset.sort;
|
const key = button.dataset.sort;
|
||||||
const active = key === currentSort.key;
|
const active = key === currentSort.key;
|
||||||
@@ -558,7 +619,8 @@ const overviewHTML = `<!doctype html>
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderIPs(items) {
|
function renderIPs(items) {
|
||||||
const rows = sortItems(items).map(item => [
|
const filteredItems = items.filter(item => showKnownBots || !item.bot);
|
||||||
|
const rows = sortItems(filteredItems).map(item => [
|
||||||
'<tr>',
|
'<tr>',
|
||||||
' <td class="mono"><div class="ip-cell">' + renderBotChip(item.bot) + '<a href="/ips/' + encodeURIComponent(item.ip) + '">' + escapeHtml(item.ip) + '</a></div></td>',
|
' <td class="mono"><div class="ip-cell">' + renderBotChip(item.bot) + '<a href="/ips/' + encodeURIComponent(item.ip) + '">' + escapeHtml(item.ip) + '</a></div></td>',
|
||||||
' <td>' + escapeHtml(item.source_name || '—') + '</td>',
|
' <td>' + escapeHtml(item.source_name || '—') + '</td>',
|
||||||
@@ -569,7 +631,8 @@ const overviewHTML = `<!doctype html>
|
|||||||
' <td>' + renderActions(item) + '</td>',
|
' <td>' + renderActions(item) + '</td>',
|
||||||
'</tr>'
|
'</tr>'
|
||||||
].join(''));
|
].join(''));
|
||||||
document.getElementById('ips-body').innerHTML = rows.length > 0 ? rows.join('') : '<tr><td colspan="7" class="muted">No IPs seen in the last 24 hours.</td></tr>';
|
const emptyMessage = showKnownBots ? 'No IPs seen in the last 24 hours.' : 'No non-bot IPs seen in the last 24 hours.';
|
||||||
|
document.getElementById('ips-body').innerHTML = rows.length > 0 ? rows.join('') : '<tr><td colspan="7" class="muted">' + escapeHtml(emptyMessage) + '</td></tr>';
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
@@ -584,6 +647,14 @@ const overviewHTML = `<!doctype html>
|
|||||||
currentSort.key = key;
|
currentSort.key = key;
|
||||||
currentSort.direction = (key === 'events' || key === 'last_seen') ? 'desc' : 'asc';
|
currentSort.direction = (key === 'events' || key === 'last_seen') ? 'desc' : 'asc';
|
||||||
}
|
}
|
||||||
|
saveSortPreference();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleKnownBots() {
|
||||||
|
const toggle = document.getElementById('show-bots-toggle');
|
||||||
|
showKnownBots = !toggle || toggle.checked;
|
||||||
|
saveShowKnownBotsPreference(showKnownBots);
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ func TestHandlerServesOverviewAndManualActions(t *testing.T) {
|
|||||||
if strings.Contains(recorder.Body.String(), "Recent events") {
|
if strings.Contains(recorder.Body.String(), "Recent events") {
|
||||||
t.Fatalf("overview page should no longer render recent events block")
|
t.Fatalf("overview page should no longer render recent events block")
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(recorder.Body.String(), "Show known bots") {
|
||||||
|
t.Fatalf("overview page should expose the known bots toggle")
|
||||||
|
}
|
||||||
|
if !strings.Contains(recorder.Body.String(), "localStorage") {
|
||||||
|
t.Fatalf("overview page should persist preferences in localStorage")
|
||||||
|
}
|
||||||
|
|
||||||
recorder = httptest.NewRecorder()
|
recorder = httptest.NewRecorder()
|
||||||
request = httptest.NewRequest(http.MethodGet, "/ips/203.0.113.10", nil)
|
request = httptest.NewRequest(http.MethodGet, "/ips/203.0.113.10", nil)
|
||||||
|
|||||||
Reference in New Issue
Block a user