Skip to content

Commit 646d71a

Browse files
committed
feat(admin): add dev-only nuke all button to KiloClaw admin page
Add a button (visible only in development mode) to destroy all active KiloClaw instances, tearing down Fly machines via the worker. Uses the same mark-then-revert pattern as single-instance destroy to keep DB and Fly state consistent on partial failures.
1 parent 63924db commit 646d71a

2 files changed

Lines changed: 119 additions & 2 deletions

File tree

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

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useCallback, useMemo, useState } from 'react';
44
import { useRouter, useSearchParams } from 'next/navigation';
5-
import { useQuery } from '@tanstack/react-query';
5+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
66
import { useTRPC } from '@/lib/trpc/utils';
77
import {
88
Table,
@@ -23,7 +23,17 @@ import {
2323
SelectValue,
2424
} from '@/components/ui/select';
2525
import { Badge } from '@/components/ui/badge';
26-
import { ChevronLeft, ChevronRight, X } from 'lucide-react';
26+
import {
27+
AlertDialog,
28+
AlertDialogAction,
29+
AlertDialogCancel,
30+
AlertDialogContent,
31+
AlertDialogDescription,
32+
AlertDialogFooter,
33+
AlertDialogHeader,
34+
AlertDialogTitle,
35+
} from '@/components/ui/alert-dialog';
36+
import { ChevronLeft, ChevronRight, X, Bomb } from 'lucide-react';
2737
import Link from 'next/link';
2838
import { formatDistanceToNow, format, parseISO } from 'date-fns';
2939
import {
@@ -219,6 +229,66 @@ function DailyChart({ data }: { data: DailyChartData[] }) {
219229
);
220230
}
221231

232+
// --- Dev Nuke All Button ---
233+
234+
function DevNukeAllButton() {
235+
if (process.env.NODE_ENV !== 'development') return null;
236+
237+
const trpc = useTRPC();
238+
const queryClient = useQueryClient();
239+
const [open, setOpen] = useState(false);
240+
241+
const nukeAll = useMutation(
242+
trpc.admin.kiloclawInstances.devNukeAll.mutationOptions({
243+
onSuccess(data) {
244+
void queryClient.invalidateQueries({
245+
queryKey: trpc.admin.kiloclawInstances.list.queryKey(),
246+
});
247+
void queryClient.invalidateQueries({
248+
queryKey: trpc.admin.kiloclawInstances.stats.queryKey(),
249+
});
250+
const errorSuffix =
251+
data.errors.length > 0
252+
? `\n${data.errors.length} failed:\n${data.errors.map(e => ` ${e.userId}: ${e.error}`).join('\n')}`
253+
: '';
254+
alert(`Destroyed ${data.destroyed}/${data.total} instances${errorSuffix}`);
255+
},
256+
})
257+
);
258+
259+
return (
260+
<>
261+
<Button variant="destructive" onClick={() => setOpen(true)} disabled={nukeAll.isPending}>
262+
<Bomb className="mr-2 h-4 w-4" />
263+
{nukeAll.isPending ? 'Nuking...' : 'Nuke All'}
264+
</Button>
265+
<AlertDialog open={open} onOpenChange={setOpen}>
266+
<AlertDialogContent>
267+
<AlertDialogHeader>
268+
<AlertDialogTitle>Nuke all KiloClaw instances?</AlertDialogTitle>
269+
<AlertDialogDescription>
270+
This will destroy every active KiloClaw instance. This action cannot be undone. Only
271+
available in development mode.
272+
</AlertDialogDescription>
273+
</AlertDialogHeader>
274+
<AlertDialogFooter>
275+
<AlertDialogCancel>Cancel</AlertDialogCancel>
276+
<AlertDialogAction
277+
onClick={() => {
278+
nukeAll.mutate();
279+
setOpen(false);
280+
}}
281+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
282+
>
283+
Nuke All
284+
</AlertDialogAction>
285+
</AlertDialogFooter>
286+
</AlertDialogContent>
287+
</AlertDialog>
288+
</>
289+
);
290+
}
291+
222292
// --- Main Page ---
223293

224294
export function KiloclawInstancesPage() {
@@ -378,6 +448,8 @@ export function KiloclawInstancesPage() {
378448
<SelectItem value="destroyed">Destroyed Only</SelectItem>
379449
</SelectContent>
380450
</Select>
451+
452+
<DevNukeAllButton />
381453
</div>
382454

383455
{/* Table */}

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,51 @@ export const adminKiloclawInstancesRouter = createTRPCRouter({
595595
}
596596
}),
597597

598+
devNukeAll: adminProcedure.mutation(async ({ ctx }) => {
599+
if (process.env.NODE_ENV !== 'development') {
600+
throw new TRPCError({
601+
code: 'FORBIDDEN',
602+
message: 'This endpoint is only available in development mode',
603+
});
604+
}
605+
606+
const activeInstances = await db
607+
.select({
608+
id: kiloclaw_instances.id,
609+
user_id: kiloclaw_instances.user_id,
610+
})
611+
.from(kiloclaw_instances)
612+
.where(isNull(kiloclaw_instances.destroyed_at));
613+
614+
console.log(
615+
`[admin-kiloclaw] DevNukeAll triggered by admin ${ctx.user.id} (${ctx.user.google_user_email}): ${activeInstances.length} active instances`
616+
);
617+
618+
const client = new KiloClawInternalClient();
619+
let destroyed = 0;
620+
const errors: Array<{ userId: string; error: string }> = [];
621+
622+
for (const instance of activeInstances) {
623+
const destroyedRow = await markActiveInstanceDestroyed(instance.user_id);
624+
try {
625+
await client.destroy(instance.user_id);
626+
destroyed++;
627+
} catch (err) {
628+
if (destroyedRow) {
629+
await restoreDestroyedInstance(destroyedRow.id);
630+
}
631+
const message = err instanceof Error ? err.message : 'Unknown error';
632+
errors.push({ userId: instance.user_id, error: message });
633+
console.error(
634+
`[admin-kiloclaw] DevNukeAll: failed to destroy instance ${instance.id} (user: ${instance.user_id}):`,
635+
err
636+
);
637+
}
638+
}
639+
640+
return { total: activeInstances.length, destroyed, errors };
641+
}),
642+
598643
reassociateVolume: adminProcedure
599644
.input(
600645
z.object({

0 commit comments

Comments
 (0)