From 34d6d3ddcb8a13b6769c6884e9c82575e58f957d Mon Sep 17 00:00:00 2001 From: "Codex, agent ChatGPT" Date: Thu, 12 Mar 2026 12:01:09 +0100 Subject: [PATCH] Add review filter and promote decisions panel --- internal/web/handler.go | 57 +++++++++++++++++++++++++++++------- internal/web/handler_test.go | 12 ++++++++ 2 files changed, 58 insertions(+), 11 deletions(-) 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 = `
+
Last 24 hours ยท click a column to sort
@@ -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 => [ '', '
' + renderBotChip(item.bot) + '' + escapeHtml(item.ip) + '
', @@ -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

- - - - - -
TimeSourceHostMethodURIStatusDecisionUser agent
-

Decisions

@@ -825,6 +851,15 @@ const ipDetailsHTML = `
+
+

Requests from this IP

+ + + + + +
TimeSourceHostMethodURIStatusDecisionUser 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 {