Skip to content

Commit f998129

Browse files
feat(admin): add safety identifier backfill panel (#1863)
* feat(admin): add safety identifier backfill panel * feat(admin): show Vercel Downstream Safety Identifier in user admin UI * refactor(admin): merge safety identifier backfill into single API call * refactor(admin): single query for safety identifier backfill * fmt * Delete obsolete script --------- Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> Co-authored-by: Christiaan Arnoldus <christiaan.arnoldus@outlook.com>
1 parent bd87483 commit f998129

File tree

7 files changed

+222
-40
lines changed

7 files changed

+222
-40
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { NextResponse } from 'next/server';
2+
import { getUserFromAuth } from '@/lib/user.server';
3+
import { db } from '@/lib/drizzle';
4+
import { kilocode_users } from '@kilocode/db';
5+
import {
6+
generateOpenRouterUpstreamSafetyIdentifier,
7+
generateVercelDownstreamSafetyIdentifier,
8+
} from '@/lib/providerHash';
9+
import { isNull, count, or, desc, eq } from 'drizzle-orm';
10+
11+
const missingEither = or(
12+
isNull(kilocode_users.openrouter_upstream_safety_identifier),
13+
isNull(kilocode_users.vercel_downstream_safety_identifier)
14+
);
15+
16+
export type SafetyIdentifierCountsResponse = {
17+
missing: number;
18+
};
19+
20+
export type BackfillBatchResponse = {
21+
processed: number;
22+
remaining: boolean;
23+
};
24+
25+
export async function GET(): Promise<
26+
NextResponse<SafetyIdentifierCountsResponse | { error: string }>
27+
> {
28+
const { authFailedResponse } = await getUserFromAuth({ adminOnly: true });
29+
if (authFailedResponse) return authFailedResponse;
30+
31+
const [result] = await db.select({ count: count() }).from(kilocode_users).where(missingEither);
32+
33+
return NextResponse.json({ missing: result?.count ?? 0 });
34+
}
35+
36+
export async function POST(): Promise<NextResponse<BackfillBatchResponse | { error: string }>> {
37+
const { authFailedResponse } = await getUserFromAuth({ adminOnly: true });
38+
if (authFailedResponse) return authFailedResponse;
39+
40+
const processed = await db.transaction(async tran => {
41+
const rows = await tran
42+
.select({ id: kilocode_users.id })
43+
.from(kilocode_users)
44+
.where(missingEither)
45+
.orderBy(desc(kilocode_users.created_at))
46+
.limit(1000);
47+
48+
for (const user of rows) {
49+
const openrouter_upstream_safety_identifier = generateOpenRouterUpstreamSafetyIdentifier(
50+
user.id
51+
);
52+
if (openrouter_upstream_safety_identifier === null) {
53+
return null;
54+
}
55+
await tran
56+
.update(kilocode_users)
57+
.set({
58+
openrouter_upstream_safety_identifier,
59+
vercel_downstream_safety_identifier: generateVercelDownstreamSafetyIdentifier(user.id),
60+
})
61+
.where(eq(kilocode_users.id, user.id))
62+
.execute();
63+
}
64+
65+
return rows.length;
66+
});
67+
68+
if (processed === null) {
69+
return NextResponse.json(
70+
{ error: 'OPENROUTER_ORG_ID is not configured on this server' },
71+
{ status: 500 }
72+
);
73+
}
74+
75+
return NextResponse.json({ processed, remaining: processed === 1000 });
76+
}

src/app/admin/components/AppSidebar.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
Bell,
2424
Server,
2525
Network,
26+
KeyRound,
2627
} from 'lucide-react';
2728
import { useSession } from 'next-auth/react';
2829
import type { Session } from 'next-auth';
@@ -79,6 +80,11 @@ const userManagementItems: MenuItem[] = [
7980
url: '/admin/blacklisted-domains',
8081
icon: () => <Shield />,
8182
},
83+
{
84+
title: () => 'Safety Identifiers',
85+
url: '/admin/safety-identifiers',
86+
icon: () => <KeyRound />,
87+
},
8288
];
8389

8490
const financialItems: MenuItem[] = [
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
5+
import { Button } from '@/components/ui/button';
6+
import { Alert, AlertDescription } from '@/components/ui/alert';
7+
import { Badge } from '@/components/ui/badge';
8+
import type {
9+
SafetyIdentifierCountsResponse,
10+
BackfillBatchResponse,
11+
} from '../api/safety-identifiers/route';
12+
13+
type BatchLog = {
14+
processed: number;
15+
timestamp: Date;
16+
};
17+
18+
export function SafetyIdentifiersBackfill() {
19+
const [logs, setLogs] = useState<BatchLog[]>([]);
20+
const queryClient = useQueryClient();
21+
22+
const { data: counts, isLoading } = useQuery<SafetyIdentifierCountsResponse>({
23+
queryKey: ['safety-identifier-counts'],
24+
queryFn: async () => {
25+
const res = await fetch('/admin/api/safety-identifiers');
26+
return res.json() as Promise<SafetyIdentifierCountsResponse>;
27+
},
28+
refetchInterval: false,
29+
});
30+
31+
const mutation = useMutation<BackfillBatchResponse, Error>({
32+
mutationFn: async () => {
33+
const res = await fetch('/admin/api/safety-identifiers', { method: 'POST' });
34+
if (!res.ok) {
35+
const body = (await res.json()) as { error?: string };
36+
throw new Error(body.error ?? `HTTP ${res.status}`);
37+
}
38+
return res.json() as Promise<BackfillBatchResponse>;
39+
},
40+
onSuccess: data => {
41+
setLogs(prev => [{ processed: data.processed, timestamp: new Date() }, ...prev]);
42+
void queryClient.invalidateQueries({ queryKey: ['safety-identifier-counts'] });
43+
},
44+
});
45+
46+
const isDone = counts?.missing === 0;
47+
48+
return (
49+
<div className="space-y-6">
50+
<p className="text-muted-foreground text-sm">
51+
Backfill safety identifiers for users missing either field. Each click processes up to 1 000
52+
users. Click repeatedly until the counter reaches zero.
53+
</p>
54+
55+
<div className="bg-background rounded-lg border p-6 space-y-4">
56+
<div className="flex items-center gap-3">
57+
<span className="font-medium">Users missing a safety identifier</span>
58+
{isLoading ? (
59+
<Badge variant="secondary">Loading…</Badge>
60+
) : isDone ? (
61+
<Badge variant="default" className="bg-green-600">
62+
All filled
63+
</Badge>
64+
) : (
65+
<Badge variant="destructive">{(counts?.missing ?? 0).toLocaleString()} missing</Badge>
66+
)}
67+
</div>
68+
69+
{mutation.isError && (
70+
<Alert variant="destructive">
71+
<AlertDescription>{mutation.error.message}</AlertDescription>
72+
</Alert>
73+
)}
74+
75+
<Button
76+
onClick={() => mutation.mutate()}
77+
disabled={isLoading || isDone || mutation.isPending}
78+
variant={isDone ? 'outline' : 'default'}
79+
>
80+
{mutation.isPending ? 'Backfilling…' : isDone ? 'Nothing to do' : 'Backfill next 1 000'}
81+
</Button>
82+
</div>
83+
84+
{logs.length > 0 && (
85+
<div className="bg-background rounded-lg border p-4 space-y-2">
86+
<h4 className="text-sm font-medium">Batch log</h4>
87+
<div className="space-y-1 font-mono text-xs">
88+
{logs.map((log, i) => (
89+
<div key={i} className="text-muted-foreground flex gap-2">
90+
<span className="shrink-0">{log.timestamp.toLocaleTimeString()}</span>
91+
<span>processed {log.processed.toLocaleString()} users</span>
92+
</div>
93+
))}
94+
</div>
95+
</div>
96+
)}
97+
</div>
98+
);
99+
}

src/app/admin/components/UserAdmin/UserAdminAccountInfo.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,21 @@ export function UserAdminAccountInfo(user: UserAdminAccountInfoProps) {
108108
<p className="text-muted-foreground">N/A</p>
109109
)}
110110
</div>
111+
<div>
112+
<h4 className="text-muted-foreground text-sm font-medium">
113+
Vercel Downstream Safety Identifier
114+
</h4>
115+
{user.vercel_downstream_safety_identifier ? (
116+
<div className="flex items-center gap-2">
117+
<p className="font-mono text-sm break-all">
118+
{user.vercel_downstream_safety_identifier}
119+
</p>
120+
<CopyTextButton text={user.vercel_downstream_safety_identifier} />
121+
</div>
122+
) : (
123+
<p className="text-muted-foreground">N/A</p>
124+
)}
125+
</div>
111126
</div>
112127
</div>
113128
</CardContent>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { SafetyIdentifiersBackfill } from '../components/SafetyIdentifiersBackfill';
2+
import AdminPage from '../components/AdminPage';
3+
import { BreadcrumbItem, BreadcrumbPage } from '@/components/ui/breadcrumb';
4+
5+
const breadcrumbs = (
6+
<>
7+
<BreadcrumbItem>
8+
<BreadcrumbPage>Safety Identifiers</BreadcrumbPage>
9+
</BreadcrumbItem>
10+
</>
11+
);
12+
13+
export default function SafetyIdentifiersPage() {
14+
return (
15+
<AdminPage breadcrumbs={breadcrumbs}>
16+
<div className="flex w-full flex-col gap-y-4">
17+
<div className="flex items-center justify-between">
18+
<h2 className="text-2xl font-bold">Safety Identifier Backfill</h2>
19+
</div>
20+
<SafetyIdentifiersBackfill />
21+
</div>
22+
</AdminPage>
23+
);
24+
}

src/lib/user.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ describe('User', () => {
9292
linkedin_url: 'https://linkedin.com/in/testuser',
9393
github_url: 'https://github.com/testuser',
9494
openrouter_upstream_safety_identifier: 'openrouter_upstream_safety_identifier',
95+
vercel_downstream_safety_identifier: 'vercel_downstream_safety_identifier',
9596
customer_source: 'A YouTube video',
9697
is_admin: true,
9798
});
@@ -108,6 +109,7 @@ describe('User', () => {
108109
expect(softDeleted!.github_url).toBeNull();
109110
expect(softDeleted!.discord_server_membership_verified_at).toBeNull();
110111
expect(softDeleted!.openrouter_upstream_safety_identifier).toBeNull();
112+
expect(softDeleted!.vercel_downstream_safety_identifier).toBeNull();
111113
expect(softDeleted!.customer_source).toBeNull();
112114
expect(softDeleted!.api_token_pepper).toBeNull();
113115
expect(softDeleted!.default_model).toBeNull();

src/scripts/openrouter/backfill-safety-identifier.ts

Lines changed: 0 additions & 40 deletions
This file was deleted.

0 commit comments

Comments
 (0)