@@ -134,6 +134,8 @@ export default function DecisionAnalysis() {
134134 const [ searchQuery , setSearchQuery ] = useState ( '' )
135135 const [ hideDuplicates , setHideDuplicates ] = useState ( false )
136136 const [ hideExpired , setHideExpired ] = useState ( true )
137+ const [ offenderPage , setOffenderPage ] = useState ( 0 )
138+ const OFFENDER_PAGE_SIZE = 20
137139 const { query, setQuery } = useSearch ( )
138140 const { lastEvent } = useSSE ( '/api/events/sse' )
139141 const seenRealtimeEventsRef = useRef < Set < string > > ( new Set ( ) )
@@ -176,6 +178,11 @@ export default function DecisionAnalysis() {
176178 refetchInterval : 60000 ,
177179 } )
178180
181+ const allOffenders = repeatedOffendersData ?. offenders ?? [ ]
182+ const offenderTotalPages = Math . ceil ( allOffenders . length / OFFENDER_PAGE_SIZE )
183+ const safePage = Math . min ( offenderPage , Math . max ( 0 , offenderTotalPages - 1 ) )
184+ const pagedOffenders = allOffenders . slice ( safePage * OFFENDER_PAGE_SIZE , ( safePage + 1 ) * OFFENDER_PAGE_SIZE )
185+
179186 // Build a lookup map: alert_id -> AlertSource
180187 const alertSourceMap = useMemo ( ( ) => {
181188 const map = new Map < number , AlertSource > ( )
@@ -458,35 +465,62 @@ export default function DecisionAnalysis() {
458465 </ CardDescription >
459466 </ CardHeader >
460467 < CardContent >
461- { repeatedOffendersData ?. offenders && repeatedOffendersData . offenders . length > 0 ? (
462- < div className = "rounded-md border overflow-x-auto" >
463- < Table >
464- < TableHeader >
465- < TableRow >
466- < TableHead > Value</ TableHead >
467- < TableHead > Scope</ TableHead >
468- < TableHead > Hits</ TableHead >
469- < TableHead > First Seen</ TableHead >
470- < TableHead > Last Seen</ TableHead >
471- < TableHead > Last Notified</ TableHead >
472- </ TableRow >
473- </ TableHeader >
474- < TableBody >
475- { repeatedOffendersData . offenders . map ( ( offender : RepeatedOffender ) => (
476- < TableRow key = { `${ offender . value } -${ offender . scope } ` } >
477- < TableCell className = "font-mono text-sm" > { offender . value } </ TableCell >
478- < TableCell > < Badge variant = "outline" > { offender . scope } </ Badge > </ TableCell >
479- < TableCell > { offender . hit_count } </ TableCell >
480- < TableCell > < TimeDisplay date = { offender . first_decision_at } /> </ TableCell >
481- < TableCell > < TimeDisplay date = { offender . last_decision_at } /> </ TableCell >
482- < TableCell >
483- { offender . last_notified_at ? < TimeDisplay date = { offender . last_notified_at } /> : 'Never' }
484- </ TableCell >
468+ { allOffenders . length > 0 ? (
469+ < >
470+ < div className = "rounded-md border overflow-x-auto" >
471+ < Table >
472+ < TableHeader >
473+ < TableRow >
474+ < TableHead > Value</ TableHead >
475+ < TableHead > Scope</ TableHead >
476+ < TableHead > Hits</ TableHead >
477+ < TableHead > First Seen</ TableHead >
478+ < TableHead > Last Seen</ TableHead >
479+ < TableHead > Last Notified</ TableHead >
485480 </ TableRow >
486- ) ) }
487- </ TableBody >
488- </ Table >
489- </ div >
481+ </ TableHeader >
482+ < TableBody >
483+ { pagedOffenders . map ( ( offender : RepeatedOffender ) => (
484+ < TableRow key = { `${ offender . value } -${ offender . scope } ` } >
485+ < TableCell className = "font-mono text-sm" > { offender . value } </ TableCell >
486+ < TableCell > < Badge variant = "outline" > { offender . scope } </ Badge > </ TableCell >
487+ < TableCell > { offender . hit_count } </ TableCell >
488+ < TableCell > < TimeDisplay date = { offender . first_decision_at } /> </ TableCell >
489+ < TableCell > < TimeDisplay date = { offender . last_decision_at } /> </ TableCell >
490+ < TableCell >
491+ { offender . last_notified_at ? < TimeDisplay date = { offender . last_notified_at } /> : 'Never' }
492+ </ TableCell >
493+ </ TableRow >
494+ ) ) }
495+ </ TableBody >
496+ </ Table >
497+ </ div >
498+ { allOffenders . length > OFFENDER_PAGE_SIZE && (
499+ < div className = "flex items-center justify-between px-2 pt-3" >
500+ < span className = "text-sm text-muted-foreground" >
501+ Page { safePage + 1 } of { offenderTotalPages } · { allOffenders . length } total
502+ </ span >
503+ < div className = "flex gap-2" >
504+ < Button
505+ variant = "outline"
506+ size = "sm"
507+ disabled = { safePage === 0 }
508+ onClick = { ( ) => setOffenderPage ( p => Math . max ( 0 , p - 1 ) ) }
509+ >
510+ Previous
511+ </ Button >
512+ < Button
513+ variant = "outline"
514+ size = "sm"
515+ disabled = { safePage >= offenderTotalPages - 1 }
516+ onClick = { ( ) => setOffenderPage ( p => Math . min ( offenderTotalPages - 1 , p + 1 ) ) }
517+ >
518+ Next
519+ </ Button >
520+ </ div >
521+ </ div >
522+ ) }
523+ </ >
490524 ) : (
491525 < div className = "text-sm text-muted-foreground" > No repeated offenders detected in the last 30 days.</ div >
492526 ) }
0 commit comments