Skip to content

Commit b87dc38

Browse files
committed
feat(admin): add machine start/stop controls to kiloclaw instance detail
Allow admins to directly start/stop Fly machines regardless of current state, to aid recovery when the DO and machine state are out of sync.
1 parent 830107f commit b87dc38

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

src/app/admin/components/KiloclawInstances/KiloclawInstanceDetail.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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>

src/routers/admin-kiloclaw-instances-router.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,28 @@ export const adminKiloclawInstancesRouter = createTRPCRouter({
448448
}
449449
}),
450450

451+
machineStart: adminProcedure.input(GatewayProcessSchema).mutation(async ({ input }) => {
452+
const fallbackMessage = 'Failed to start machine';
453+
try {
454+
const client = new KiloClawInternalClient();
455+
return await client.start(input.userId);
456+
} catch (err) {
457+
console.error('Failed to start machine for user:', input.userId, err);
458+
throwKiloclawAdminError(err, fallbackMessage);
459+
}
460+
}),
461+
462+
machineStop: adminProcedure.input(GatewayProcessSchema).mutation(async ({ input }) => {
463+
const fallbackMessage = 'Failed to stop machine';
464+
try {
465+
const client = new KiloClawInternalClient();
466+
return await client.stop(input.userId);
467+
} catch (err) {
468+
console.error('Failed to stop machine for user:', input.userId, err);
469+
throwKiloclawAdminError(err, fallbackMessage);
470+
}
471+
}),
472+
451473
destroy: adminProcedure.input(DestroyInstanceSchema).mutation(async ({ input, ctx }) => {
452474
const [instance] = await db
453475
.select({

0 commit comments

Comments
 (0)