You've already forked caddy-opnsense-blocker
Add review filter and promote decisions panel
This commit is contained in:
@@ -399,6 +399,7 @@ const overviewHTML = `<!doctype html>
|
|||||||
<div class="toolbar-right">
|
<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-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-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 class="meta">Last 24 hours · click a column to sort</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -423,6 +424,7 @@ const overviewHTML = `<!doctype html>
|
|||||||
const storageKeys = {
|
const storageKeys = {
|
||||||
showKnownBots: 'caddy-opnsense-blocker.overview.showKnownBots',
|
showKnownBots: 'caddy-opnsense-blocker.overview.showKnownBots',
|
||||||
showAllowed: 'caddy-opnsense-blocker.overview.showAllowed',
|
showAllowed: 'caddy-opnsense-blocker.overview.showAllowed',
|
||||||
|
showReviewOnly: 'caddy-opnsense-blocker.overview.showReviewOnly',
|
||||||
sortKey: 'caddy-opnsense-blocker.overview.sortKey',
|
sortKey: 'caddy-opnsense-blocker.overview.sortKey',
|
||||||
sortDirection: 'caddy-opnsense-blocker.overview.sortDirection',
|
sortDirection: 'caddy-opnsense-blocker.overview.sortDirection',
|
||||||
};
|
};
|
||||||
@@ -439,6 +441,7 @@ const overviewHTML = `<!doctype html>
|
|||||||
let currentSort = loadSortPreference();
|
let currentSort = loadSortPreference();
|
||||||
let showKnownBots = loadShowKnownBotsPreference();
|
let showKnownBots = loadShowKnownBotsPreference();
|
||||||
let showAllowed = loadShowAllowedPreference();
|
let showAllowed = loadShowAllowedPreference();
|
||||||
|
let showReviewOnly = loadShowReviewOnlyPreference();
|
||||||
|
|
||||||
function renderStats(data) {
|
function renderStats(data) {
|
||||||
const stats = [
|
const stats = [
|
||||||
@@ -499,6 +502,25 @@ const overviewHTML = `<!doctype html>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadShowReviewOnlyPreference() {
|
||||||
|
try {
|
||||||
|
const rawValue = window.localStorage.getItem(storageKeys.showReviewOnly);
|
||||||
|
if (rawValue === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return rawValue === 'true';
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveShowReviewOnlyPreference(value) {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(storageKeys.showReviewOnly, value ? 'true' : 'false');
|
||||||
|
} catch (_) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function loadSortPreference() {
|
function loadSortPreference() {
|
||||||
const fallback = { key: 'events', direction: 'desc' };
|
const fallback = { key: 'events', direction: 'desc' };
|
||||||
try {
|
try {
|
||||||
@@ -593,6 +615,10 @@ const overviewHTML = `<!doctype html>
|
|||||||
if (allowedToggle) {
|
if (allowedToggle) {
|
||||||
allowedToggle.checked = showAllowed;
|
allowedToggle.checked = showAllowed;
|
||||||
}
|
}
|
||||||
|
const reviewToggle = document.getElementById('show-review-toggle');
|
||||||
|
if (reviewToggle) {
|
||||||
|
reviewToggle.checked = showReviewOnly;
|
||||||
|
}
|
||||||
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;
|
||||||
@@ -645,7 +671,7 @@ const overviewHTML = `<!doctype html>
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderIPs(items) {
|
function renderIPs(items) {
|
||||||
const filteredItems = items.filter(item => (showKnownBots || !item.bot) && (showAllowed || item.state !== 'allowed'));
|
const filteredItems = items.filter(item => (showKnownBots || !item.bot) && (showAllowed || item.state !== 'allowed') && (!showReviewOnly || item.state === 'review'));
|
||||||
const rows = sortItems(filteredItems).map(item => [
|
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>',
|
||||||
@@ -658,7 +684,9 @@ const overviewHTML = `<!doctype html>
|
|||||||
'</tr>'
|
'</tr>'
|
||||||
].join(''));
|
].join(''));
|
||||||
let emptyMessage = 'No IPs seen in the last 24 hours.';
|
let emptyMessage = 'No IPs seen in the last 24 hours.';
|
||||||
if (!showKnownBots && !showAllowed) {
|
if (showReviewOnly) {
|
||||||
|
emptyMessage = 'No review IPs match the current filters in the last 24 hours.';
|
||||||
|
} else if (!showKnownBots && !showAllowed) {
|
||||||
emptyMessage = 'No non-bot, non-allowed IPs seen in the last 24 hours.';
|
emptyMessage = 'No non-bot, non-allowed IPs seen in the last 24 hours.';
|
||||||
} else if (!showKnownBots) {
|
} else if (!showKnownBots) {
|
||||||
emptyMessage = 'No non-bot IPs seen in the last 24 hours.';
|
emptyMessage = 'No non-bot IPs seen in the last 24 hours.';
|
||||||
@@ -698,6 +726,13 @@ const overviewHTML = `<!doctype html>
|
|||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleReviewOnly() {
|
||||||
|
const toggle = document.getElementById('show-review-toggle');
|
||||||
|
showReviewOnly = !!toggle && toggle.checked;
|
||||||
|
saveShowReviewOnlyPreference(showReviewOnly);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
async function sendAction(ip, action, promptLabel) {
|
async function sendAction(ip, action, promptLabel) {
|
||||||
const reason = window.prompt(promptLabel, '');
|
const reason = window.prompt(promptLabel, '');
|
||||||
if (reason === null) {
|
if (reason === null) {
|
||||||
@@ -807,15 +842,6 @@ const ipDetailsHTML = `<!doctype html>
|
|||||||
<h2>Investigation</h2>
|
<h2>Investigation</h2>
|
||||||
<div id="investigation" class="kv"></div>
|
<div id="investigation" class="kv"></div>
|
||||||
</section>
|
</section>
|
||||||
<section class="panel">
|
|
||||||
<h2>Requests from this IP</h2>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>Time</th><th>Source</th><th>Host</th><th>Method</th><th>URI</th><th>Status</th><th>Decision</th><th>User agent</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="events-body"></tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>Decisions</h2>
|
<h2>Decisions</h2>
|
||||||
<table>
|
<table>
|
||||||
@@ -825,6 +851,15 @@ const ipDetailsHTML = `<!doctype html>
|
|||||||
<tbody id="decisions-body"></tbody>
|
<tbody id="decisions-body"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Requests from this IP</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Time</th><th>Source</th><th>Host</th><th>Method</th><th>URI</th><th>Status</th><th>Decision</th><th>User agent</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="events-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>Backend actions</h2>
|
<h2>Backend actions</h2>
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ func TestHandlerServesOverviewAndManualActions(t *testing.T) {
|
|||||||
if !strings.Contains(recorder.Body.String(), "Show allowed") {
|
if !strings.Contains(recorder.Body.String(), "Show allowed") {
|
||||||
t.Fatalf("overview page should expose the allowed toggle")
|
t.Fatalf("overview page should expose the allowed toggle")
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(recorder.Body.String(), "Review only") {
|
||||||
|
t.Fatalf("overview page should expose the review-only toggle")
|
||||||
|
}
|
||||||
if !strings.Contains(recorder.Body.String(), "localStorage") {
|
if !strings.Contains(recorder.Body.String(), "localStorage") {
|
||||||
t.Fatalf("overview page should persist preferences in localStorage")
|
t.Fatalf("overview page should persist preferences in localStorage")
|
||||||
}
|
}
|
||||||
@@ -103,6 +106,15 @@ func TestHandlerServesOverviewAndManualActions(t *testing.T) {
|
|||||||
if strings.Contains(body, `refresh().then(() => investigate());`) {
|
if strings.Contains(body, `refresh().then(() => investigate());`) {
|
||||||
t.Fatalf("ip details page should not auto-refresh investigation on load")
|
t.Fatalf("ip details page should not auto-refresh investigation on load")
|
||||||
}
|
}
|
||||||
|
investigationIndex := strings.Index(body, "<h2>Investigation</h2>")
|
||||||
|
decisionsIndex := strings.Index(body, "<h2>Decisions</h2>")
|
||||||
|
requestsIndex := strings.Index(body, "<h2>Requests from this IP</h2>")
|
||||||
|
if investigationIndex == -1 || decisionsIndex == -1 || requestsIndex == -1 {
|
||||||
|
t.Fatalf("ip details page is missing expected sections")
|
||||||
|
}
|
||||||
|
if !(investigationIndex < decisionsIndex && decisionsIndex < requestsIndex) {
|
||||||
|
t.Fatalf("expected Decisions block right after Investigation block")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type stubApp struct {
|
type stubApp struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user