Skip to content

Commit 675983a

Browse files
committed
Add node registration API and UI
1 parent 816afe4 commit 675983a

File tree

3 files changed

+98
-0
lines changed

3 files changed

+98
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { NextResponse } from 'next/server'
2+
import { eq } from 'drizzle-orm'
3+
import { getDb } from '@/lib/db'
4+
import { adminServers } from '@sandchest/db/schema'
5+
import { bytesToId, NODE_PREFIX } from '@sandchest/contract'
6+
7+
export async function POST(
8+
_request: Request,
9+
{ params }: { params: Promise<{ serverId: string }> },
10+
) {
11+
const { serverId } = await params
12+
const db = getDb()
13+
const serverIdBuf = Buffer.from(serverId, 'hex') as unknown as Uint8Array
14+
15+
const [server] = await db
16+
.select()
17+
.from(adminServers)
18+
.where(eq(adminServers.id, serverIdBuf))
19+
.limit(1)
20+
21+
if (!server) {
22+
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
23+
}
24+
25+
if (!server.nodeId) {
26+
return NextResponse.json({ error: 'No node ID — deploy the daemon first' }, { status: 400 })
27+
}
28+
29+
const nodeId = bytesToId(NODE_PREFIX, server.nodeId as unknown as Uint8Array)
30+
const apiUrl = process.env.API_URL ?? 'https://api.sandchest.com'
31+
const apiToken = process.env.ADMIN_API_TOKEN
32+
33+
const res = await fetch(`${apiUrl}/v1/admin/nodes`, {
34+
method: 'POST',
35+
headers: {
36+
'Content-Type': 'application/json',
37+
...(apiToken ? { Authorization: `Bearer ${apiToken}` } : {}),
38+
},
39+
body: JSON.stringify({
40+
id: nodeId,
41+
name: server.name ?? server.ip,
42+
hostname: server.ip,
43+
}),
44+
})
45+
46+
if (!res.ok) {
47+
const text = await res.text().catch(() => '')
48+
return NextResponse.json(
49+
{ error: `API registration failed (${res.status}): ${text}` },
50+
{ status: 502 },
51+
)
52+
}
53+
54+
const data = await res.json() as Record<string, unknown>
55+
return NextResponse.json({
56+
success: true,
57+
node_id: nodeId,
58+
hostname: server.ip,
59+
upserted: data.upserted ?? false,
60+
})
61+
}

apps/admin/src/app/servers/[serverId]/page.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,20 @@ export default function ServerDetailPage({
151151
},
152152
})
153153

154+
const registerMutation = useMutation({
155+
mutationFn: async () => {
156+
const res = await fetch(`/api/servers/${serverId}/register-node`, { method: 'POST' })
157+
if (!res.ok) {
158+
const data = await res.json().catch(() => ({ error: 'Registration failed' })) as { error: string }
159+
throw new Error(data.error)
160+
}
161+
return res.json() as Promise<{ node_id: string; hostname: string; upserted: boolean }>
162+
},
163+
onSuccess: () => {
164+
queryClient.invalidateQueries({ queryKey: ['server', serverId] })
165+
},
166+
})
167+
154168
const destroyAllVmsMutation = useMutation({
155169
mutationFn: async () => {
156170
const res = await fetch(`/api/servers/${serverId}/destroy-all-vms`, { method: 'POST' })
@@ -359,6 +373,13 @@ export default function ServerDetailPage({
359373
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
360374
<button
361375
className="btn btn-primary btn-sm"
376+
onClick={() => registerMutation.mutate()}
377+
disabled={registerMutation.isPending}
378+
>
379+
{registerMutation.isPending ? <><span className="spinner" style={{ width: '0.75rem', height: '0.75rem', marginRight: '0.375rem' }} /> Registering…</> : 'Register Node'}
380+
</button>
381+
<button
382+
className="btn btn-sm"
362383
onClick={() => deployMutation.mutate()}
363384
disabled={deployMutation.isPending}
364385
>
@@ -407,6 +428,17 @@ export default function ServerDetailPage({
407428
All VMs destroyed and daemon restarted.
408429
</div>
409430
)}
431+
{registerMutation.isError && (
432+
<div className="feedback-card feedback-danger" style={{ marginTop: '0.75rem' }}>
433+
{registerMutation.error.message}
434+
</div>
435+
)}
436+
{registerMutation.isSuccess && (
437+
<div className="feedback-card feedback-success" style={{ marginTop: '0.75rem' }}>
438+
Node registered — hostname: {server.ip}, ID: {registerMutation.data?.node_id}
439+
{registerMutation.data?.upserted ? ' (updated)' : ' (created)'}
440+
</div>
441+
)}
410442
</div>
411443
)}
412444

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import AdminShell from '@/components/AdminShell'
2+
3+
export default function SmokeLayout({ children }: { children: React.ReactNode }) {
4+
return <AdminShell>{children}</AdminShell>
5+
}

0 commit comments

Comments
 (0)