diff --git a/internal/web/handler.go b/internal/web/handler.go
index 9b58575..79c74bf 100644
--- a/internal/web/handler.go
+++ b/internal/web/handler.go
@@ -399,6 +399,7 @@ const overviewHTML = `
@@ -423,6 +424,7 @@ const overviewHTML = `
const storageKeys = {
showKnownBots: 'caddy-opnsense-blocker.overview.showKnownBots',
showAllowed: 'caddy-opnsense-blocker.overview.showAllowed',
+ showReviewOnly: 'caddy-opnsense-blocker.overview.showReviewOnly',
sortKey: 'caddy-opnsense-blocker.overview.sortKey',
sortDirection: 'caddy-opnsense-blocker.overview.sortDirection',
};
@@ -439,6 +441,7 @@ const overviewHTML = `
let currentSort = loadSortPreference();
let showKnownBots = loadShowKnownBotsPreference();
let showAllowed = loadShowAllowedPreference();
+ let showReviewOnly = loadShowReviewOnlyPreference();
function renderStats(data) {
const stats = [
@@ -499,6 +502,25 @@ const overviewHTML = `
}
}
+ 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() {
const fallback = { key: 'events', direction: 'desc' };
try {
@@ -593,6 +615,10 @@ const overviewHTML = `
if (allowedToggle) {
allowedToggle.checked = showAllowed;
}
+ const reviewToggle = document.getElementById('show-review-toggle');
+ if (reviewToggle) {
+ reviewToggle.checked = showReviewOnly;
+ }
document.querySelectorAll('button[data-sort]').forEach(button => {
const key = button.dataset.sort;
const active = key === currentSort.key;
@@ -645,7 +671,7 @@ const overviewHTML = `
}
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 => [
'',
' | ',
@@ -658,7 +684,9 @@ const overviewHTML = `
'
'
].join(''));
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.';
} else if (!showKnownBots) {
emptyMessage = 'No non-bot IPs seen in the last 24 hours.';
@@ -698,6 +726,13 @@ const overviewHTML = `
render();
}
+ function toggleReviewOnly() {
+ const toggle = document.getElementById('show-review-toggle');
+ showReviewOnly = !!toggle && toggle.checked;
+ saveShowReviewOnlyPreference(showReviewOnly);
+ render();
+ }
+
async function sendAction(ip, action, promptLabel) {
const reason = window.prompt(promptLabel, '');
if (reason === null) {
@@ -807,15 +842,6 @@ const ipDetailsHTML = `
Investigation
-
- Requests from this IP
-
-
- | Time | Source | Host | Method | URI | Status | Decision | User agent |
-
-
-
-
Decisions
@@ -825,6 +851,15 @@ const ipDetailsHTML = `
+
+ Requests from this IP
+
+
+ | Time | Source | Host | Method | URI | Status | Decision | User agent |
+
+
+
+
Backend actions
diff --git a/internal/web/handler_test.go b/internal/web/handler_test.go
index 820384f..a81dd18 100644
--- a/internal/web/handler_test.go
+++ b/internal/web/handler_test.go
@@ -83,6 +83,9 @@ func TestHandlerServesOverviewAndManualActions(t *testing.T) {
if !strings.Contains(recorder.Body.String(), "Show allowed") {
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") {
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());`) {
t.Fatalf("ip details page should not auto-refresh investigation on load")
}
+ investigationIndex := strings.Index(body, "Investigation
")
+ decisionsIndex := strings.Index(body, "Decisions
")
+ requestsIndex := strings.Index(body, "Requests from this IP
")
+ 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 {