You've already forked caddy-opnsense-blocker
Add background IP intel and restore dashboard stats
This commit is contained in:
@@ -337,13 +337,16 @@ const overviewHTML = `<!doctype html>
|
||||
:root { color-scheme: dark; }
|
||||
body { font-family: system-ui, sans-serif; margin: 0; background: #0f172a; color: #e2e8f0; }
|
||||
header { padding: 1rem 1.5rem; border-bottom: 1px solid #334155; position: sticky; top: 0; background: rgba(15,23,42,.97); }
|
||||
main { padding: 1.5rem; }
|
||||
main { padding: 1.5rem; display: grid; gap: 1.25rem; }
|
||||
h1, h2 { margin: 0 0 .75rem 0; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { padding: .55rem .65rem; border-bottom: 1px solid #1e293b; text-align: left; vertical-align: top; }
|
||||
th { color: #93c5fd; white-space: nowrap; }
|
||||
a { color: #93c5fd; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: .75rem; }
|
||||
.card { background: #111827; border: 1px solid #334155; border-radius: .75rem; padding: .9rem; }
|
||||
.stat-value { font-size: 1.7rem; font-weight: 700; }
|
||||
.status { display: inline-block; padding: .15rem .45rem; border-radius: 999px; font-size: .8rem; background: #1e293b; }
|
||||
.status.blocked { background: #7f1d1d; }
|
||||
.status.review { background: #78350f; }
|
||||
@@ -362,6 +365,22 @@ const overviewHTML = `<!doctype html>
|
||||
button { background: #2563eb; color: white; border: 0; cursor: pointer; }
|
||||
button.secondary { background: #475569; }
|
||||
button.danger { background: #dc2626; }
|
||||
.ip-cell { display: flex; align-items: center; gap: .45rem; }
|
||||
.bot-chip { display: inline-flex; align-items: center; justify-content: center; width: 1.25rem; height: 1.25rem; border-radius: 999px; border: 1px solid #334155; background: #0f172a; color: #e2e8f0; font-size: .72rem; font-weight: 700; cursor: help; }
|
||||
.bot-chip.verified { border-color: #2563eb; }
|
||||
.bot-chip.hint { border-style: dashed; }
|
||||
.bot-chip.google { background: #2563eb; color: white; }
|
||||
.bot-chip.bing { background: #0284c7; color: white; }
|
||||
.bot-chip.apple { background: #475569; color: white; }
|
||||
.bot-chip.meta { background: #2563eb; color: white; }
|
||||
.bot-chip.duckduckgo { background: #ea580c; color: white; }
|
||||
.bot-chip.openai { background: #059669; color: white; }
|
||||
.bot-chip.anthropic { background: #b45309; color: white; }
|
||||
.bot-chip.perplexity { background: #0f766e; color: white; }
|
||||
.bot-chip.semrush { background: #db2777; color: white; }
|
||||
.bot-chip.yandex { background: #dc2626; color: white; }
|
||||
.bot-chip.baidu { background: #7c3aed; color: white; }
|
||||
.bot-chip.bytespider { background: #111827; color: white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -370,6 +389,7 @@ const overviewHTML = `<!doctype html>
|
||||
<div class="muted">Local-only review and enforcement console</div>
|
||||
</header>
|
||||
<main>
|
||||
<section class="stats" id="stats"></section>
|
||||
<section class="panel">
|
||||
<div class="toolbar">
|
||||
<h2>Recent IPs</h2>
|
||||
@@ -405,6 +425,23 @@ const overviewHTML = `<!doctype html>
|
||||
let currentItems = [];
|
||||
let currentSort = { key: 'events', direction: 'desc' };
|
||||
|
||||
function renderStats(data) {
|
||||
const stats = [
|
||||
['Total events', data.total_events],
|
||||
['Tracked IPs', data.total_ips],
|
||||
['Blocked', data.blocked_ips],
|
||||
['Review', data.review_ips],
|
||||
['Allowed', data.allowed_ips],
|
||||
['Observed', data.observed_ips],
|
||||
];
|
||||
document.getElementById('stats').innerHTML = stats.map(([label, value]) => [
|
||||
'<div class="card">',
|
||||
' <div class="muted">' + escapeHtml(label) + '</div>',
|
||||
' <div class="stat-value">' + escapeHtml(value) + '</div>',
|
||||
'</div>'
|
||||
].join('')).join('');
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '').replace(/[&<>"']/g, character => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[character]));
|
||||
}
|
||||
@@ -433,6 +470,41 @@ const overviewHTML = `<!doctype html>
|
||||
return leftRank - rightRank;
|
||||
}
|
||||
|
||||
function botVisual(bot) {
|
||||
const candidate = String((bot || {}).provider_id || (bot || {}).name || '').toLowerCase();
|
||||
const catalog = [
|
||||
{ match: ['google'], short: 'G', className: 'google' },
|
||||
{ match: ['bing', 'microsoft'], short: 'B', className: 'bing' },
|
||||
{ match: ['apple'], short: 'A', className: 'apple' },
|
||||
{ match: ['facebook', 'meta'], short: 'M', className: 'meta' },
|
||||
{ match: ['duckduckgo', 'duckduckbot'], short: 'D', className: 'duckduckgo' },
|
||||
{ match: ['gptbot', 'openai'], short: 'O', className: 'openai' },
|
||||
{ match: ['claudebot', 'anthropic'], short: 'C', className: 'anthropic' },
|
||||
{ match: ['perplexity'], short: 'P', className: 'perplexity' },
|
||||
{ match: ['semrush'], short: 'S', className: 'semrush' },
|
||||
{ match: ['yandex'], short: 'Y', className: 'yandex' },
|
||||
{ match: ['baidu'], short: 'B', className: 'baidu' },
|
||||
{ match: ['bytespider', 'tiktok'], short: 'T', className: 'bytespider' },
|
||||
];
|
||||
for (const entry of catalog) {
|
||||
if (entry.match.some(fragment => candidate.includes(fragment))) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
const name = String((bot || {}).name || '').trim();
|
||||
return { short: (name[0] || '?').toUpperCase(), className: 'generic' };
|
||||
}
|
||||
|
||||
function renderBotChip(bot) {
|
||||
if (!bot) {
|
||||
return '';
|
||||
}
|
||||
const visual = botVisual(bot);
|
||||
const statusClass = bot.verified ? 'verified' : 'hint';
|
||||
const title = (bot.name || 'Bot') + (bot.verified ? '' : ' (possible)');
|
||||
return '<span class="bot-chip ' + escapeHtml(visual.className) + ' ' + statusClass + '" title="' + escapeHtml(title) + '">' + escapeHtml(visual.short) + '</span>';
|
||||
}
|
||||
|
||||
function updateSortButtons() {
|
||||
document.querySelectorAll('button[data-sort]').forEach(button => {
|
||||
const key = button.dataset.sort;
|
||||
@@ -488,7 +560,7 @@ const overviewHTML = `<!doctype html>
|
||||
function renderIPs(items) {
|
||||
const rows = sortItems(items).map(item => [
|
||||
'<tr>',
|
||||
' <td class="mono"><a href="/ips/' + encodeURIComponent(item.ip) + '">' + escapeHtml(item.ip) + '</a></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><span class="status ' + escapeHtml(item.state) + '">' + escapeHtml(item.state) + '</span></td>',
|
||||
' <td>' + escapeHtml(item.events) + '</td>',
|
||||
@@ -534,14 +606,21 @@ const overviewHTML = `<!doctype html>
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const response = await fetch('/api/recent-ips?hours=' + recentHours + '&limit=250');
|
||||
const payload = await response.json().catch(() => []);
|
||||
if (!response.ok) {
|
||||
const message = Array.isArray(payload) ? response.statusText : (payload.error || response.statusText);
|
||||
const [overviewResponse, recentResponse] = await Promise.all([
|
||||
fetch('/api/overview?limit=50'),
|
||||
fetch('/api/recent-ips?hours=' + recentHours + '&limit=250')
|
||||
]);
|
||||
const overviewPayload = await overviewResponse.json().catch(() => ({}));
|
||||
const recentPayload = await recentResponse.json().catch(() => []);
|
||||
if (overviewResponse.ok) {
|
||||
renderStats(overviewPayload || {});
|
||||
}
|
||||
if (!recentResponse.ok) {
|
||||
const message = Array.isArray(recentPayload) ? recentResponse.statusText : (recentPayload.error || recentResponse.statusText);
|
||||
document.getElementById('ips-body').innerHTML = '<tr><td colspan="7" class="muted">' + escapeHtml(message) + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
currentItems = Array.isArray(payload) ? payload : [];
|
||||
currentItems = Array.isArray(recentPayload) ? recentPayload : [];
|
||||
render();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user