import { useCallback, useState } from 'react' import { Loading, ErrorState } from '../../../components/PageState.jsx' import { useAsync } from '../../../lib/useAsync.js' import { dateTime } from '../../../lib/format.js' import { api } from '../../../api/client.js' // Read-only visibility into the botScore middleware: who is currently banned and // a feed of recent scoring events. The only action is an emergency unban for // false positives — there is no ban/adjust-weights surface here by design. const mono = { fontFamily: 'ui-monospace,Menlo,monospace', fontSize: '0.82rem' } export default function BotActivityAdmin() { const [tick, setTick] = useState(0) const reload = useCallback(() => setTick((t) => t + 1), []) const { loading, error, data } = useAsync(() => api.admin.botActivity(), [tick]) const [busyIp, setBusyIp] = useState('') const ips = data?.ips || [] const events = data?.events || [] const banned = ips.filter((e) => e.banned) async function unban(ip) { if (!window.confirm(`Unban ${ip}? This clears its score and ban immediately.`)) return setBusyIp(ip) try { await api.admin.unbanIp(ip) reload() } catch { // Surface nothing intrusive; a reload will re-fetch true state either way. reload() } finally { setBusyIp('') } } if (loading) return if (error) return return (

Live, in-memory scoring and ban state from the bot-protection middleware. State resets when the server restarts.

{/* Currently banned IPs */}

Currently banned{banned.length > 0 ? ` (${banned.length})` : ''}

{banned.length === 0 && ( )} {banned.map((e) => ( ))}
IP Score Banned until
No IPs are currently banned.
{e.ip} {e.score} {dateTime(e.bannedUntil)}
{/* Recent scoring events */}

Recent events

{events.length === 0 && ( )} {events.map((ev) => ( ))}
When IP Reason Path Points Score
No events recorded yet.
{dateTime(ev.ts)} {ev.ip} {ev.reason} {ev.path || '—'} {ev.points ? `+${ev.points}` : '—'} {ev.score}
) }