diff --git a/src/app/admin/components/KiloclawInstances/KiloclawInstancesPage.tsx b/src/app/admin/components/KiloclawInstances/KiloclawInstancesPage.tsx index 814abff7e..464b518dd 100644 --- a/src/app/admin/components/KiloclawInstances/KiloclawInstancesPage.tsx +++ b/src/app/admin/components/KiloclawInstances/KiloclawInstancesPage.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useTRPC } from '@/lib/trpc/utils'; import { Table, @@ -23,7 +23,17 @@ import { SelectValue, } from '@/components/ui/select'; import { Badge } from '@/components/ui/badge'; -import { ChevronLeft, ChevronRight, X } from 'lucide-react'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { ChevronLeft, ChevronRight, X, Bomb } from 'lucide-react'; import Link from 'next/link'; import { formatDistanceToNow, format, parseISO } from 'date-fns'; import { @@ -219,6 +229,66 @@ function DailyChart({ data }: { data: DailyChartData[] }) { ); } +// --- Dev Nuke All Button --- + +function DevNukeAllButton() { + if (process.env.NODE_ENV !== 'development') return null; + + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); + + const nukeAll = useMutation( + trpc.admin.kiloclawInstances.devNukeAll.mutationOptions({ + onSuccess(data) { + void queryClient.invalidateQueries({ + queryKey: trpc.admin.kiloclawInstances.list.queryKey(), + }); + void queryClient.invalidateQueries({ + queryKey: trpc.admin.kiloclawInstances.stats.queryKey(), + }); + const errorSuffix = + data.errors.length > 0 + ? `\n${data.errors.length} failed:\n${data.errors.map(e => ` ${e.userId}: ${e.error}`).join('\n')}` + : ''; + alert(`Destroyed ${data.destroyed}/${data.total} instances${errorSuffix}`); + }, + }) + ); + + return ( + <> + + + + + Nuke all KiloClaw instances? + + This will destroy every active KiloClaw instance. This action cannot be undone. Only + available in development mode. + + + + Cancel + { + nukeAll.mutate(); + setOpen(false); + }} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + Nuke All + + + + + + ); +} + // --- Main Page --- export function KiloclawInstancesPage() { @@ -378,6 +448,8 @@ export function KiloclawInstancesPage() { Destroyed Only + + {/* Table */} diff --git a/src/routers/admin-kiloclaw-instances-router.ts b/src/routers/admin-kiloclaw-instances-router.ts index 28e5a95fc..1e620b38f 100644 --- a/src/routers/admin-kiloclaw-instances-router.ts +++ b/src/routers/admin-kiloclaw-instances-router.ts @@ -595,6 +595,51 @@ export const adminKiloclawInstancesRouter = createTRPCRouter({ } }), + devNukeAll: adminProcedure.mutation(async ({ ctx }) => { + if (process.env.NODE_ENV !== 'development') { + throw new TRPCError({ + code: 'FORBIDDEN', + message: 'This endpoint is only available in development mode', + }); + } + + const activeInstances = await db + .select({ + id: kiloclaw_instances.id, + user_id: kiloclaw_instances.user_id, + }) + .from(kiloclaw_instances) + .where(isNull(kiloclaw_instances.destroyed_at)); + + console.log( + `[admin-kiloclaw] DevNukeAll triggered by admin ${ctx.user.id} (${ctx.user.google_user_email}): ${activeInstances.length} active instances` + ); + + const client = new KiloClawInternalClient(); + let destroyed = 0; + const errors: Array<{ userId: string; error: string }> = []; + + for (const instance of activeInstances) { + const destroyedRow = await markActiveInstanceDestroyed(instance.user_id); + try { + await client.destroy(instance.user_id); + destroyed++; + } catch (err) { + if (destroyedRow) { + await restoreDestroyedInstance(destroyedRow.id); + } + const message = err instanceof Error ? err.message : 'Unknown error'; + errors.push({ userId: instance.user_id, error: message }); + console.error( + `[admin-kiloclaw] DevNukeAll: failed to destroy instance ${instance.id} (user: ${instance.user_id}):`, + err + ); + } + } + + return { total: activeInstances.length, destroyed, errors }; + }), + reassociateVolume: adminProcedure .input( z.object({