Recent IPs
@@ -456,7 +516,7 @@ const overviewHTML = `
Actions |
-
+
| Loading recent IPs… |
@@ -478,7 +538,7 @@ const overviewHTML = `
reason: 'Reason',
};
const stateOrder = { blocked: 0, review: 1, observed: 2, allowed: 3 };
- let currentItems = [];
+ let currentItems = null;
let currentSort = loadSortPreference();
let showKnownBots = loadShowKnownBotsPreference();
let showAllowed = loadShowAllowedPreference();
@@ -675,7 +735,7 @@ const overviewHTML = `
return [
'
',
' ',
- '
' + escapeHtml(item.ip) + '',
+ '
',
'
' + escapeHtml(primaryValue) + '',
'
',
''
@@ -811,6 +871,10 @@ const overviewHTML = `
}
function renderIPs(items) {
+ if (!Array.isArray(items)) {
+ document.getElementById('ips-body').innerHTML = '
| Loading recent IPs… |
';
+ return;
+ }
const filteredItems = items.filter(item => (showKnownBots || !item.bot) && (showAllowed || item.state !== 'allowed') && (!showReviewOnly || item.state === 'review'));
const rows = sortItems(filteredItems).map(item => [
'
',
@@ -905,9 +969,11 @@ const overviewHTML = `
}
async function refresh() {
- const recentResponse = await fetch('/api/recent-ips?hours=' + recentHours + '&limit=250');
+ const [_, recentResponse] = await Promise.all([
+ refreshOverview(),
+ 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 = '
| ' + escapeHtml(message) + ' |
';
@@ -933,7 +999,7 @@ const ipDetailsHTML = `
diff --git a/internal/web/handler_test.go b/internal/web/handler_test.go
index c24d9f2..b3b37d1 100644
--- a/internal/web/handler_test.go
+++ b/internal/web/handler_test.go
@@ -102,9 +102,15 @@ func TestHandlerServesOverviewAndManualActions(t *testing.T) {
if !strings.Contains(recorder.Body.String(), "Top URLs by events") {
t.Fatalf("overview page should expose the top URLs block")
}
+ if !strings.Contains(recorder.Body.String(), "Loading…") {
+ t.Fatalf("overview page should render stable loading placeholders")
+ }
if !strings.Contains(recorder.Body.String(), "These two filters affect both the leaderboards and the Recent IPs list") {
t.Fatalf("overview page should explain the scope of the shared filters")
}
+ if strings.Contains(recorder.Body.String(), "position: sticky") {
+ t.Fatalf("overview page header should no longer be sticky")
+ }
if !strings.Contains(recorder.Body.String(), "Show allowed") {
t.Fatalf("overview page should expose the allowed toggle")
}
@@ -131,6 +137,9 @@ func TestHandlerServesOverviewAndManualActions(t *testing.T) {
if strings.Contains(body, `refresh().then(() => investigate());`) {
t.Fatalf("ip details page should not auto-refresh investigation on load")
}
+ if strings.Contains(body, "position: sticky") {
+ t.Fatalf("ip details page header should no longer be sticky")
+ }
investigationIndex := strings.Index(body, "
Investigation
")
decisionsIndex := strings.Index(body, "
Decisions
")
requestsIndex := strings.Index(body, "
Requests from this IP
")