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({