diff --git a/kiloclaw/worker-configuration.d.ts b/kiloclaw/worker-configuration.d.ts index 0e0b0f4a1..7de4792f9 100644 --- a/kiloclaw/worker-configuration.d.ts +++ b/kiloclaw/worker-configuration.d.ts @@ -14,7 +14,7 @@ declare namespace Cloudflare { FLY_REGISTRY_APP: "kiloclaw-machines"; FLY_ORG_SLUG: "kilo-679"; FLY_IMAGE_TAG: "latest"; - FLY_REGION: "dfw,ewr,lax,sjc,eu"; + FLY_REGION: "eu,us"; OPENCLAW_ALLOWED_ORIGINS: "https://claw.kilosessions.ai,https://kilo.ai,https://www.kilo.ai"; REQUIRE_PROXY_TOKEN: "true"; PROACTIVE_REFRESH_THRESHOLD_HOURS: "72"; diff --git a/src/app/admin/components/UserAdmin/UserAdminKiloClaw.tsx b/src/app/admin/components/UserAdmin/UserAdminKiloClaw.tsx index ec027e2a9..18c2e644b 100644 --- a/src/app/admin/components/UserAdmin/UserAdminKiloClaw.tsx +++ b/src/app/admin/components/UserAdmin/UserAdminKiloClaw.tsx @@ -2,6 +2,8 @@ import { useEffect, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import Link from 'next/link'; +import { ExternalLink } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; @@ -139,8 +141,20 @@ export function UserAdminKiloClaw({ userId }: { userId: string }) { return ( - KiloClaw - No KiloClaw subscription +
+
+ KiloClaw + n/a +
+ {data?.activeInstanceId && ( + + )} +
{data?.earlybird ? ( @@ -185,11 +199,21 @@ export function UserAdminKiloClaw({ userId }: { userId: string }) { KiloClaw KiloClaw subscription and trial status - {canEditTrialEnd && ( - - )} +
+ {data.activeInstanceId && ( + + )} + {canEditTrialEnd && ( + + )} +
diff --git a/src/routers/admin-kiloclaw-user-router.test.ts b/src/routers/admin-kiloclaw-user-router.test.ts index ab3d81465..56dc765eb 100644 --- a/src/routers/admin-kiloclaw-user-router.test.ts +++ b/src/routers/admin-kiloclaw-user-router.test.ts @@ -5,6 +5,7 @@ import { insertTestUser } from '@/tests/helpers/user.helper'; import { kiloclaw_admin_audit_logs, kiloclaw_earlybird_purchases, + kiloclaw_instances, kiloclaw_subscriptions, } from '@kilocode/db/schema'; import { eq } from 'drizzle-orm'; @@ -53,6 +54,7 @@ describe('admin.users.getKiloClawState', () => { hasAccess: expectedAccessWithoutEntitlement, accessReason: null, earlybird: null, + activeInstanceId: null, }); }); @@ -133,6 +135,34 @@ describe('admin.users.getKiloClawState', () => { }) ); }); + + it('returns activeInstanceId when the user has an active instance', async () => { + const [instance] = await db + .insert(kiloclaw_instances) + .values({ + user_id: targetUser.id, + sandbox_id: 'sandbox-test-active', + }) + .returning({ id: kiloclaw_instances.id }); + + const caller = await createCallerForUser(adminUser.id); + const result = await caller.admin.users.getKiloClawState({ userId: targetUser.id }); + + expect(result.activeInstanceId).toBe(instance.id); + }); + + it('returns null activeInstanceId when the user only has destroyed instances', async () => { + await db.insert(kiloclaw_instances).values({ + user_id: targetUser.id, + sandbox_id: 'sandbox-test-destroyed', + destroyed_at: new Date().toISOString(), + }); + + const caller = await createCallerForUser(adminUser.id); + const result = await caller.admin.users.getKiloClawState({ userId: targetUser.id }); + + expect(result.activeInstanceId).toBeNull(); + }); }); describe('admin.users.updateKiloClawTrialEndAt', () => { diff --git a/src/routers/admin-router.ts b/src/routers/admin-router.ts index 4a988c8fd..ccb6f2ff7 100644 --- a/src/routers/admin-router.ts +++ b/src/routers/admin-router.ts @@ -469,7 +469,7 @@ export const adminRouter = createTRPCRouter({ throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' }); } - const [subscription, earlybird] = await Promise.all([ + const [subscription, earlybird, activeInstance] = await Promise.all([ db.query.kiloclaw_subscriptions.findFirst({ where: eq(kiloclaw_subscriptions.user_id, input.userId), }), @@ -477,6 +477,13 @@ export const adminRouter = createTRPCRouter({ columns: { id: true }, where: eq(kiloclaw_earlybird_purchases.user_id, input.userId), }), + db.query.kiloclaw_instances.findFirst({ + columns: { id: true }, + where: and( + eq(kiloclaw_instances.user_id, input.userId), + isNull(kiloclaw_instances.destroyed_at) + ), + }), ]); const now = new Date(); @@ -518,6 +525,7 @@ export const adminRouter = createTRPCRouter({ daysRemaining: earlybirdDaysRemaining, } : null, + activeInstanceId: activeInstance?.id ?? null, }; }),