@@ -394,6 +394,36 @@ export function KiloclawInstanceDetail({ instanceId }: { instanceId: string }) {
394394 void queryClient . invalidateQueries ( { queryKey : trpc . admin . kiloclawInstances . get . queryKey ( ) } ) ;
395395 } ;
396396
397+ const machineControlsEnabled = data ?. destroyed_at === null && ! ! data ?. workerStatus ?. flyMachineId ;
398+
399+ const invalidateMachineQueries = ( ) => {
400+ void queryClient . invalidateQueries ( { queryKey : trpc . admin . kiloclawInstances . get . queryKey ( ) } ) ;
401+ } ;
402+
403+ const { mutateAsync : machineStart , isPending : isMachineStarting } = useMutation (
404+ trpc . admin . kiloclawInstances . machineStart . mutationOptions ( {
405+ onSuccess : ( ) => {
406+ toast . success ( 'Machine start requested' ) ;
407+ invalidateMachineQueries ( ) ;
408+ } ,
409+ onError : err => {
410+ toast . error ( `Failed to start machine: ${ err . message } ` ) ;
411+ } ,
412+ } )
413+ ) ;
414+
415+ const { mutateAsync : machineStop , isPending : isMachineStopping } = useMutation (
416+ trpc . admin . kiloclawInstances . machineStop . mutationOptions ( {
417+ onSuccess : ( ) => {
418+ toast . success ( 'Machine stop requested' ) ;
419+ invalidateMachineQueries ( ) ;
420+ } ,
421+ onError : err => {
422+ toast . error ( `Failed to stop machine: ${ err . message } ` ) ;
423+ } ,
424+ } )
425+ ) ;
426+
397427 const { mutateAsync : gatewayStart , isPending : isGatewayStarting } = useMutation (
398428 trpc . admin . kiloclawInstances . gatewayStart . mutationOptions ( {
399429 onSuccess : ( ) => {
@@ -494,6 +524,7 @@ export function KiloclawInstanceDetail({ instanceId }: { instanceId: string }) {
494524 }
495525
496526 const isActive = data . destroyed_at === null ;
527+ const machineActionPending = isMachineStarting || isMachineStopping ;
497528 const gatewayActionPending =
498529 isGatewayStarting ||
499530 isGatewayStopping ||
@@ -790,6 +821,51 @@ export function KiloclawInstanceDetail({ instanceId }: { instanceId: string }) {
790821 </ CardContent >
791822 </ Card >
792823
824+ { /* Machine Controls */ }
825+ { isActive && machineControlsEnabled && (
826+ < Card >
827+ < CardHeader >
828+ < div className = "flex items-center justify-between gap-3" >
829+ < div >
830+ < CardTitle > Machine Controls</ CardTitle >
831+ < CardDescription > Start or stop the Fly machine</ CardDescription >
832+ </ div >
833+ < StatusBadge status = { data . workerStatus ?. status ?? null } />
834+ </ div >
835+ </ CardHeader >
836+ < CardContent >
837+ < div className = "flex flex-wrap gap-2" >
838+ < Button
839+ size = "sm"
840+ variant = "outline"
841+ disabled = { machineActionPending }
842+ onClick = { ( ) => void machineStart ( { userId : data . user_id } ) }
843+ >
844+ { isMachineStarting ? (
845+ < Loader2 className = "mr-1 h-4 w-4 animate-spin" />
846+ ) : (
847+ < Play className = "mr-1 h-4 w-4" />
848+ ) }
849+ Start Machine
850+ </ Button >
851+ < Button
852+ size = "sm"
853+ variant = "outline"
854+ disabled = { machineActionPending }
855+ onClick = { ( ) => void machineStop ( { userId : data . user_id } ) }
856+ >
857+ { isMachineStopping ? (
858+ < Loader2 className = "mr-1 h-4 w-4 animate-spin" />
859+ ) : (
860+ < Square className = "mr-1 h-4 w-4" />
861+ ) }
862+ Stop Machine
863+ </ Button >
864+ </ div >
865+ </ CardContent >
866+ </ Card >
867+ ) }
868+
793869 { /* Gateway Process (controller) */ }
794870 { isActive && (
795871 < Card >
0 commit comments