Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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 (
<>
<Button variant="destructive" onClick={() => setOpen(true)} disabled={nukeAll.isPending}>
<Bomb className="mr-2 h-4 w-4" />
{nukeAll.isPending ? 'Nuking...' : 'Nuke All'}
</Button>
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Nuke all KiloClaw instances?</AlertDialogTitle>
<AlertDialogDescription>
This will destroy every active KiloClaw instance. This action cannot be undone. Only
available in development mode.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
nukeAll.mutate();
setOpen(false);
}}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Nuke All
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}

// --- Main Page ---

export function KiloclawInstancesPage() {
Expand Down Expand Up @@ -378,6 +448,8 @@ export function KiloclawInstancesPage() {
<SelectItem value="destroyed">Destroyed Only</SelectItem>
</SelectContent>
</Select>

<DevNukeAllButton />
</div>

{/* Table */}
Expand Down
45 changes: 45 additions & 0 deletions src/routers/admin-kiloclaw-instances-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading