From fcf65664bf47a66d701555d2b06f081ec12b7d29 Mon Sep 17 00:00:00 2001 From: Tracewebstudio Dev Date: Sat, 15 Aug 2026 14:20:50 +0200 Subject: [PATCH] feat(admin): add Guard Events tab to AI Management page Shows blocked AI chat messages from ai_guard_violations, paginated, filterable by guard_type (keyword / moderation_api / length / flood). Fetches from GET /api/admin/ai/guard-events. Co-Authored-By: Claude Sonnet 4.6 --- src/routes/admin/ai-management.tsx | 159 ++++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 2 deletions(-) diff --git a/src/routes/admin/ai-management.tsx b/src/routes/admin/ai-management.tsx index 852e693..a460fec 100644 --- a/src/routes/admin/ai-management.tsx +++ b/src/routes/admin/ai-management.tsx @@ -28,7 +28,7 @@ async function fetchJson(path: string, opts?: RequestInit) { } export default function AiManagementPage() { - const [activeTab, setActiveTab] = createSignal<'plans' | 'features' | 'user' | 'usage'>('plans'); + const [activeTab, setActiveTab] = createSignal<'plans' | 'features' | 'user' | 'usage' | 'guard'>('plans'); const [userId, setUserId] = createSignal(''); const [planCode, setPlanCode] = createSignal(''); const [creditAmount, setCreditAmount] = createSignal(10); @@ -75,11 +75,12 @@ export default function AiManagementPage() { } }; - const tabs: { key: 'plans' | 'features' | 'user' | 'usage'; label: string }[] = [ + const tabs: { key: 'plans' | 'features' | 'user' | 'usage' | 'guard'; label: string }[] = [ { key: 'plans', label: 'Plans' }, { key: 'features', label: 'Features' }, { key: 'user', label: 'User Management' }, { key: 'usage', label: 'Usage Lookup' }, + { key: 'guard', label: 'πŸ›‘ Guard Events' }, ]; return ( @@ -230,6 +231,160 @@ export default function AiManagementPage() { + + +
+

AI Guard Events

+

+ Messages blocked by the content guard (prompt injection keywords + OpenAI Moderation API). +

+ +
+
+ + ); +} + +// ── Guard Events panel ──────────────────────────────────────────────────────── + +const GUARD_TYPE_LABELS: Record = { + keyword: { label: 'Keyword', color: '#f59e0b' }, + moderation_api: { label: 'Moderation API', color: '#ef4444' }, + length: { label: 'Length', color: '#6b7280' }, + flood: { label: 'Flood', color: '#8b5cf6' }, +}; + +function GuardEvents() { + const [events, setEvents] = createSignal([]); + const [total, setTotal] = createSignal(0); + const [filterType, setFilterType] = createSignal(''); + const [offset, setOffset] = createSignal(0); + const [loading, setLoading] = createSignal(false); + const [err, setErr] = createSignal(''); + const LIMIT = 50; + + const load = async () => { + setLoading(true); + setErr(''); + try { + const qs = new URLSearchParams({ limit: String(LIMIT), offset: String(offset()) }); + if (filterType()) qs.set('guard_type', filterType()); + const data = await fetchJson(`/guard-events?${qs.toString()}`); + setEvents(data.events || []); + setTotal(data.total ?? 0); + } catch (e: any) { + setErr(e.message); + } finally { + setLoading(false); + } + }; + + // Load on mount and whenever filter/offset changes + const handleFilter = () => { setOffset(0); load(); }; + const handlePrev = () => { setOffset(Math.max(0, offset() - LIMIT)); load(); }; + const handleNext = () => { setOffset(offset() + LIMIT); load(); }; + + // Initial load + load(); + + return ( +
+ {/* Filter bar */} +
+ + + {total()} total events +
+ +

{err()}

+ + {/* Table */} +
+ + + + + + + + + + + + + + + + + + {(ev: any) => { + const typeInfo = GUARD_TYPE_LABELS[ev.guard_type] ?? { label: ev.guard_type, color: '#6b7280' }; + return ( + + + + + + + + ); + }} + + +
TimeTypeReasonMessage excerptUser ID
No guard events found.
+ {new Date(ev.created_at).toLocaleString()} + + + {typeInfo.label} + + {ev.reason} + {ev.message_excerpt} + + {ev.user_id ? ev.user_id.slice(0, 8) + '…' : 'β€”'} +
+
+ + {/* Pagination */} + LIMIT}> +
+ + + {offset() + 1}–{Math.min(offset() + LIMIT, total())} of {total()} + + +
+
); }