'use client' import { useState, useEffect, useRef, useCallback } from 'react' import { Toggle, toast, Spinner, getDateRange } from '@ciphera-net/ui' import { ShieldCheck } from '@phosphor-icons/react' import { useSite, useBotFilterStats, useSessions } from '@/lib/swr/dashboard' import { updateSite } from '@/lib/api/sites' import { botFilterSessions, botUnfilterSessions } from '@/lib/api/bot-filter' export default function SiteBotSpamTab({ siteId, onDirtyChange, onRegisterSave }: { siteId: string; onDirtyChange?: (dirty: boolean) => void; onRegisterSave?: (fn: () => Promise) => void }) { const { data: site, mutate } = useSite(siteId) const { data: botStats, mutate: mutateBotStats } = useBotFilterStats(siteId) const [filterBots, setFilterBots] = useState(false) const initialFilterRef = useRef(null) const [botView, setBotView] = useState<'review' | 'blocked'>('review') const [suspiciousOnly, setSuspiciousOnly] = useState(true) const [selectedSessions, setSelectedSessions] = useState>(new Set()) const [botDateRange] = useState(() => getDateRange(7)) const { data: sessionsData, mutate: mutateSessions } = useSessions(siteId, botDateRange.start, botDateRange.end, botView === 'review' ? suspiciousOnly : false) const sessions = sessionsData?.sessions const hasInitialized = useRef(false) useEffect(() => { if (!site || hasInitialized.current) return setFilterBots(site.filter_bots ?? false) initialFilterRef.current = site.filter_bots ?? false hasInitialized.current = true }, [site]) // Track dirty state useEffect(() => { if (initialFilterRef.current === null) return const dirty = filterBots !== initialFilterRef.current onDirtyChange?.(dirty) }, [filterBots, onDirtyChange]) const handleSave = useCallback(async () => { try { await updateSite(siteId, { name: site?.name || '', filter_bots: filterBots }) await mutate() initialFilterRef.current = filterBots onDirtyChange?.(false) toast.success('Bot filtering updated') } catch { toast.error('Failed to save') } }, [siteId, site?.name, filterBots, mutate, onDirtyChange]) useEffect(() => { onRegisterSave?.(handleSave) }, [handleSave, onRegisterSave]) const handleBotFilter = async (sessionIds: string[]) => { try { await botFilterSessions(siteId, sessionIds) toast.success(`${sessionIds.length} session(s) flagged as bot`) setSelectedSessions(new Set()) mutateSessions() mutateBotStats() } catch { toast.error('Failed to flag sessions') } } const handleBotUnfilter = async (sessionIds: string[]) => { try { await botUnfilterSessions(siteId, sessionIds) toast.success(`${sessionIds.length} session(s) unblocked`) setSelectedSessions(new Set()) mutateSessions() mutateBotStats() } catch { toast.error('Failed to unblock sessions') } } if (!site) return
return (

Bot & Spam Filtering

Automatically filter bot traffic and referrer spam from your analytics.

{/* Bot filtering toggle */}

Enable bot filtering

Filter known bots, crawlers, referrer spam, and suspicious traffic.

setFilterBots(p => !p)} />
{/* Stats */} {botStats && (

{botStats.filtered_sessions ?? 0}

Sessions filtered

{botStats.filtered_events ?? 0}

Events filtered

{botStats.auto_blocked_this_month ?? 0}

Auto-blocked this month

)} {/* Session Review */}

Session Review

{/* Review/Blocked toggle */}
{/* Suspicious only filter (review mode only) */} {botView === 'review' && (

Suspicious only

Show only sessions flagged as suspicious.

setSuspiciousOnly(v => !v)} />
)} {/* Bulk actions bar */} {selectedSessions.size > 0 && (
{selectedSessions.size} selected {botView === 'review' ? ( ) : ( )}
)} {/* Session cards */}
{(sessions || []) .filter(s => botView === 'blocked' ? s.bot_filtered : !s.bot_filtered) .map(session => (
{ const next = new Set(selectedSessions) e.target.checked ? next.add(session.session_id) : next.delete(session.session_id) setSelectedSessions(next) }} className="w-4 h-4 shrink-0 cursor-pointer" style={{ accentColor: '#FD5E0F' }} />
{session.first_page || '/'} {session.suspicion_score != null && ( = 5 ? 'bg-red-900/30 text-red-400' : session.suspicion_score >= 3 ? 'bg-yellow-900/30 text-yellow-400' : 'bg-neutral-800 text-neutral-400' }`}> {session.suspicion_score >= 5 ? 'High risk' : session.suspicion_score >= 3 ? 'Suspicious' : 'Low risk'} )}
{session.pageviews} page(s) {session.duration ? `${Math.round(session.duration)}s` : 'No duration'} {[session.city, session.country].filter(Boolean).join(', ') || 'Unknown location'} {session.browser || 'Unknown browser'} {session.referrer || 'Direct'}
))} {(!sessions || sessions.filter(s => botView === 'blocked' ? s.bot_filtered : !s.bot_filtered).length === 0) && (

{botView === 'blocked' ? 'No blocked sessions' : 'No suspicious sessions found'}

)}
) }