@@ -56,12 +56,17 @@ import {
5656 CheckCircle2 ,
5757 XCircle ,
5858 ShieldAlert ,
59+ Activity ,
5960} from 'lucide-react' ;
6061import Link from 'next/link' ;
6162import { formatDistanceToNow } from 'date-fns' ;
6263import { Tooltip , TooltipContent , TooltipTrigger } from '@/components/ui/tooltip' ;
6364import { toast } from 'sonner' ;
6465import { AdminFileEditor } from './AdminFileEditor' ;
66+ import {
67+ useKiloclawInstanceEvents ,
68+ type KiloclawEventRow ,
69+ } from '@/app/admin/api/kiloclaw-analytics/hooks' ;
6570
6671function formatRelativeTime ( timestamp : string | null ) : string {
6772 if ( ! timestamp ) return '—' ;
@@ -844,6 +849,135 @@ function VolumeReassociationCard({
844849 ) ;
845850}
846851
852+ function DeliveryBadge ( { delivery } : { delivery : string } ) {
853+ switch ( delivery ) {
854+ case 'do' :
855+ return (
856+ < Badge className = "bg-blue-600 text-xs" variant = "default" >
857+ do
858+ </ Badge >
859+ ) ;
860+ case 'reconcile' :
861+ return (
862+ < Badge className = "bg-amber-600 text-xs" variant = "default" >
863+ reconcile
864+ </ Badge >
865+ ) ;
866+ default :
867+ return < Badge variant = "outline" > { delivery } </ Badge > ;
868+ }
869+ }
870+
871+ function formatDuration ( ms : number ) : string {
872+ if ( ms === 0 ) return '—' ;
873+ if ( ms < 1000 ) return `${ Math . round ( ms ) } ms` ;
874+ return `${ ( ms / 1000 ) . toFixed ( 1 ) } s` ;
875+ }
876+
877+ function InstanceEventsCard ( { sandboxId } : { sandboxId : string } ) {
878+ const { data, isLoading, error } = useKiloclawInstanceEvents ( sandboxId ) ;
879+
880+ return (
881+ < Card >
882+ < CardHeader >
883+ < div className = "flex items-center gap-2" >
884+ < Activity className = "h-5 w-5" />
885+ < div >
886+ < CardTitle > DO & Reconcile Events </ CardTitle >
887+ < CardDescription > Recent events from Analytics Engine</ CardDescription >
888+ </ div >
889+ </ div >
890+ </ CardHeader >
891+ < CardContent >
892+ { isLoading && (
893+ < div className = "flex items-center gap-2" >
894+ < Loader2 className = "h-4 w-4 animate-spin" />
895+ < span className = "text-muted-foreground text-sm" > Loading events...</ span >
896+ </ div >
897+ ) }
898+
899+ { error && (
900+ < Alert >
901+ < AlertTriangle className = "h-4 w-4" />
902+ < AlertDescription >
903+ { error instanceof Error ? error . message : 'Failed to load events' }
904+ </ AlertDescription >
905+ </ Alert >
906+ ) }
907+
908+ { data && data . data . length === 0 && (
909+ < p className = "text-muted-foreground text-sm" > No DO or reconcile events found.</ p >
910+ ) }
911+
912+ { data && data . data . length > 0 && (
913+ < div className = "overflow-x-auto" >
914+ < table className = "w-full text-sm" >
915+ < thead >
916+ < tr className = "text-muted-foreground border-b text-left text-xs" >
917+ < th className = "pr-4 pb-2" > Time</ th >
918+ < th className = "pr-4 pb-2" > Event</ th >
919+ < th className = "pr-4 pb-2" > Delivery</ th >
920+ < th className = "pr-4 pb-2" > Status</ th >
921+ < th className = "pr-4 pb-2" > Label</ th >
922+ < th className = "pr-4 pb-2" > Duration</ th >
923+ < th className = "pb-2" > Error</ th >
924+ </ tr >
925+ </ thead >
926+ < tbody >
927+ { data . data . map ( ( row : KiloclawEventRow , i : number ) => (
928+ < tr key = { `${ row . timestamp } -${ i } ` } className = "border-b last:border-0" >
929+ < td className = "py-2 pr-4 whitespace-nowrap" >
930+ < Tooltip >
931+ < TooltipTrigger asChild >
932+ < span className = "text-xs" >
933+ { formatDistanceToNow ( new Date ( row . timestamp ) , { addSuffix : true } ) }
934+ </ span >
935+ </ TooltipTrigger >
936+ < TooltipContent > { new Date ( row . timestamp ) . toLocaleString ( ) } </ TooltipContent >
937+ </ Tooltip >
938+ </ td >
939+ < td className = "py-2 pr-4" >
940+ < code className = "text-xs" > { row . event } </ code >
941+ </ td >
942+ < td className = "py-2 pr-4" >
943+ < DeliveryBadge delivery = { row . delivery } />
944+ </ td >
945+ < td className = "py-2 pr-4" >
946+ < span className = "text-xs" > { row . status || '—' } </ span >
947+ </ td >
948+ < td className = "py-2 pr-4" >
949+ < span className = "text-xs" > { row . label || '—' } </ span >
950+ </ td >
951+ < td className = "py-2 pr-4 whitespace-nowrap" >
952+ < span className = "text-xs" > { formatDuration ( row . duration_ms ) } </ span >
953+ </ td >
954+ < td className = "py-2" >
955+ { row . error ? (
956+ < Tooltip >
957+ < TooltipTrigger asChild >
958+ < span className = "text-destructive block max-w-[200px] truncate text-xs" >
959+ { row . error }
960+ </ span >
961+ </ TooltipTrigger >
962+ < TooltipContent className = "max-w-[400px]" >
963+ < p className = "break-words text-xs" > { row . error } </ p >
964+ </ TooltipContent >
965+ </ Tooltip >
966+ ) : (
967+ < span className = "text-muted-foreground text-xs" > —</ span >
968+ ) }
969+ </ td >
970+ </ tr >
971+ ) ) }
972+ </ tbody >
973+ </ table >
974+ </ div >
975+ ) }
976+ </ CardContent >
977+ </ Card >
978+ ) ;
979+ }
980+
847981/** Strip ANSI escape codes so raw terminal output can render in a browser <pre>. */
848982function stripAnsi ( raw : string ) : string {
849983 // eslint-disable-next-line no-control-regex
@@ -1422,6 +1556,9 @@ export function KiloclawInstanceDetail({ instanceId }: { instanceId: string }) {
14221556 </ CardContent >
14231557 </ Card >
14241558
1559+ { /* DO & Reconcile Events */ }
1560+ { data . sandbox_id && < InstanceEventsCard sandboxId = { data . sandbox_id } /> }
1561+
14251562 { /* Machine Controls */ }
14261563 { isActive && machineControlsEnabled && (
14271564 < Card >
0 commit comments