Skip to content

Commit d3d507d

Browse files
fixes
1 parent 872e83d commit d3d507d

5 files changed

Lines changed: 114 additions & 36 deletions

File tree

src/app/api/health/[instanceId]/route.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { NextResponse } from 'next/server'
22
import { requireAuth } from '@/lib/auth'
33
import { supabaseAdmin } from '@/lib/supabase/admin'
44
import { checkInstanceHealth } from '@/lib/openclaw/health'
5+
import { createDnsRecord, isDnsConfigured } from '@/lib/dns/cloudflare'
6+
import { logInstanceEvent } from '@/lib/control-plane'
7+
8+
const INSTANCE_DOMAIN = process.env.INSTANCE_DOMAIN ?? 'agentcomputers.app'
59

610
export async function GET(
711
_req: Request,
@@ -36,6 +40,22 @@ export async function GET(
3640
updates.provisioned_at = new Date().toISOString()
3741
}
3842

43+
if (!instance.dashboard_url && instance.ip_address && health.status === 'healthy' && isDnsConfigured()) {
44+
try {
45+
const dns = await createDnsRecord(instance.slug, instance.ip_address)
46+
if (dns.success) {
47+
updates.dashboard_url = `https://${instance.slug}.${INSTANCE_DOMAIN}`
48+
await logInstanceEvent(instanceId, 'dns_created', {
49+
record_id: dns.id,
50+
hostname: `${instance.slug}.${INSTANCE_DOMAIN}`,
51+
source: 'health_check_retry',
52+
})
53+
}
54+
} catch (err) {
55+
console.error('DNS retry during health check failed:', err)
56+
}
57+
}
58+
3959
await supabaseAdmin
4060
.from('instances')
4161
.update(updates)

src/components/instances/instance-dashboard.tsx

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,46 @@
11
'use client'
22

33
import { ExternalLink, LoaderCircle } from 'lucide-react'
4-
import { useState } from 'react'
4+
import { useEffect, useRef, useState } from 'react'
55
import { buttonVariants } from '@/components/ui/button'
66
import type { Instance } from '@/types/instance'
77

8+
const LOAD_TIMEOUT_MS = 8000
9+
10+
function buildDashboardUrl(base: string, token: string | null): string {
11+
return token ? `${base}/#token=${token}` : base
12+
}
13+
814
export function InstanceDashboard({ instance }: { instance: Instance }) {
9-
const baseUrl = instance.dashboard_url ?? (instance.ip_address ? `http://${instance.ip_address}` : null)
10-
const dashboardUrl = baseUrl
11-
? instance.gateway_token
12-
? `${baseUrl}/#token=${instance.gateway_token}`
13-
: baseUrl
15+
const httpsBase = instance.dashboard_url ?? null
16+
const ipBase = instance.ip_address ? `http://${instance.ip_address}` : null
17+
const [useFallback, setUseFallback] = useState(false)
18+
const [loaded, setLoaded] = useState(false)
19+
const timerRef = useRef<ReturnType<typeof setTimeout>>(null)
20+
21+
const activeBase = useFallback ? (ipBase ?? httpsBase) : (httpsBase ?? ipBase)
22+
const dashboardUrl = activeBase
23+
? buildDashboardUrl(activeBase, instance.gateway_token)
1424
: null
1525

16-
const [loaded, setLoaded] = useState(false)
26+
useEffect(() => {
27+
return () => { if (timerRef.current) clearTimeout(timerRef.current) }
28+
}, [])
29+
30+
function startLoadTimer() {
31+
if (timerRef.current) clearTimeout(timerRef.current)
32+
if (!useFallback && httpsBase && ipBase) {
33+
timerRef.current = setTimeout(() => {
34+
setUseFallback(true)
35+
setLoaded(false)
36+
}, LOAD_TIMEOUT_MS)
37+
}
38+
}
39+
40+
function handleLoad() {
41+
if (timerRef.current) clearTimeout(timerRef.current)
42+
setLoaded(true)
43+
}
1744

1845
if (!dashboardUrl) {
1946
return (
@@ -52,12 +79,14 @@ export function InstanceDashboard({ instance }: { instance: Instance }) {
5279
</div>
5380
)}
5481
<iframe
82+
key={dashboardUrl}
5583
src={dashboardUrl}
5684
className="h-[calc(100dvh-14rem)] w-full"
5785
style={{ colorScheme: 'dark', background: 'var(--background, #0a0a0a)' }}
5886
title="OpenClaw Dashboard"
5987
allow="clipboard-read; clipboard-write"
60-
onLoad={() => setLoaded(true)}
88+
ref={(el) => { if (el) startLoadTimer() }}
89+
onLoad={handleLoad}
6190
/>
6291
</div>
6392
</div>

src/components/instances/instance-terminal-workspace.tsx

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use client'
22

3-
import { useState } from 'react'
3+
import { useEffect, useRef, useState } from 'react'
44
import { ExternalLink, LoaderCircle, Plus, RefreshCw, TerminalSquare, X } from 'lucide-react'
5-
import { Button, buttonVariants } from '@/components/ui/button'
65
import { cn } from '@/lib/utils'
76
import { getInstanceTerminalConnection } from '@/lib/terminal'
87
import type { Instance } from '@/types/instance'
98

9+
const LOAD_TIMEOUT_MS = 8000
10+
1011
interface InstanceTerminalWorkspaceProps {
1112
instance: Instance
1213
className?: string
@@ -31,6 +32,18 @@ export function InstanceTerminalWorkspace({
3132
const [activeSessionId, setActiveSessionId] = useState('terminal-1')
3233
const [nextSessionNumber, setNextSessionNumber] = useState(2)
3334
const [loadedSessions, setLoadedSessions] = useState<Record<string, boolean>>({})
35+
const [useFallback, setUseFallback] = useState(false)
36+
const loadTimers = useRef<Record<string, ReturnType<typeof setTimeout>>>({})
37+
38+
const terminalUrl = useFallback && connection.directUrl
39+
? connection.directUrl
40+
: connection.preferredUrl
41+
42+
useEffect(() => {
43+
return () => {
44+
Object.values(loadTimers.current).forEach(clearTimeout)
45+
}
46+
}, [])
3447

3548
if (instance.status !== 'running') {
3649
return (
@@ -50,7 +63,7 @@ export function InstanceTerminalWorkspace({
5063
)
5164
}
5265

53-
if (!connection.preferredUrl) {
66+
if (!terminalUrl) {
5467
return (
5568
<div className="flex min-h-[28rem] items-center justify-center rounded-xl border bg-card p-8 text-center">
5669
<div className="max-w-sm space-y-3">
@@ -64,9 +77,24 @@ export function InstanceTerminalWorkspace({
6477
)
6578
}
6679

67-
const terminalUrl = connection.preferredUrl
6880
const activeSession = sessions.find((s) => s.id === activeSessionId) ?? sessions[0]
6981

82+
function startLoadTimer(sessionId: string) {
83+
clearTimeout(loadTimers.current[sessionId])
84+
if (!useFallback && connection.directUrl && connection.mode === 'proxied') {
85+
loadTimers.current[sessionId] = setTimeout(() => {
86+
setUseFallback(true)
87+
setLoadedSessions({})
88+
setSessions((prev) => prev.map((s) => ({ ...s, version: s.version + 1 })))
89+
}, LOAD_TIMEOUT_MS)
90+
}
91+
}
92+
93+
function handleLoad(sessionId: string) {
94+
clearTimeout(loadTimers.current[sessionId])
95+
setLoadedSessions((prev) => ({ ...prev, [sessionId]: true }))
96+
}
97+
7098
function createNewSession() {
7199
const session = createSession(nextSessionNumber)
72100
setSessions((prev) => [...prev, session])
@@ -84,6 +112,7 @@ export function InstanceTerminalWorkspace({
84112

85113
function closeSession(sessionId: string) {
86114
if (sessions.length === 1) return
115+
clearTimeout(loadTimers.current[sessionId])
87116
const idx = sessions.findIndex((s) => s.id === sessionId)
88117
const remaining = sessions.filter((s) => s.id !== sessionId)
89118
setSessions(remaining)
@@ -99,9 +128,7 @@ export function InstanceTerminalWorkspace({
99128

100129
return (
101130
<div className={cn('flex flex-col gap-0 overflow-hidden rounded-xl border bg-[#0a0a0a]', className)}>
102-
{/* Toolbar */}
103131
<div className="flex items-center gap-1 border-b border-white/10 bg-[#111] px-2 py-1.5">
104-
{/* Tabs */}
105132
<div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto">
106133
{sessions.map((session) => {
107134
const isActive = session.id === activeSession.id
@@ -145,7 +172,6 @@ export function InstanceTerminalWorkspace({
145172
</button>
146173
</div>
147174

148-
{/* Actions */}
149175
<div className="flex items-center gap-1">
150176
<button
151177
type="button"
@@ -156,7 +182,7 @@ export function InstanceTerminalWorkspace({
156182
<RefreshCw className="h-3.5 w-3.5" />
157183
</button>
158184
<a
159-
href={connection.preferredUrl}
185+
href={terminalUrl}
160186
target="_blank"
161187
rel="noopener noreferrer"
162188
className="rounded-md p-1.5 text-white/40 transition-colors hover:bg-white/5 hover:text-white/70"
@@ -167,7 +193,6 @@ export function InstanceTerminalWorkspace({
167193
</div>
168194
</div>
169195

170-
{/* Terminal iframe area */}
171196
<div className="relative min-h-[calc(100dvh-16rem)] bg-[#0a0a0a]">
172197
{sessions.map((session) => {
173198
const isActive = session.id === activeSession.id
@@ -191,13 +216,14 @@ export function InstanceTerminalWorkspace({
191216
)}
192217

193218
<iframe
194-
key={`${session.id}:${session.version}`}
219+
key={`${session.id}:${session.version}:${terminalUrl}`}
195220
src={terminalUrl}
196221
title={session.title}
197222
className="block h-full w-full"
198223
style={{ colorScheme: 'dark', background: '#0a0a0a' }}
199224
allow="clipboard-read; clipboard-write; fullscreen"
200-
onLoad={() => setLoadedSessions((prev) => ({ ...prev, [session.id]: true }))}
225+
ref={(el) => { if (el && isActive) startLoadTimer(session.id) }}
226+
onLoad={() => handleLoad(session.id)}
201227
/>
202228
</div>
203229
)

src/lib/control-plane.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,7 @@ describe('control plane provisioning', () => {
139139
expect.objectContaining({
140140
gatewayToken: 'gateway-token-123',
141141
dashboardUrl: 'https://demo.agent.example',
142-
aiGatewayApiKey: 'vck_123',
143-
stripeRestrictedKey: 'rk_123',
142+
proxyBaseUrl: 'https://agentcomputers.app/api/gateway/proxy',
144143
})
145144
)
146145
expect(mockGenerateCloudInit).toHaveBeenCalledWith(

src/lib/control-plane.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,32 @@ export async function provisionInstance(
5353
},
5454
})
5555

56+
let dnsSuccess = false
57+
if (isDnsConfigured()) {
58+
try {
59+
const dns = await createDnsRecord(instance.slug, server.public_net.ipv4.ip)
60+
dnsSuccess = dns.success
61+
if (dns.success) {
62+
await logInstanceEvent(instance.id, 'dns_created', {
63+
record_id: dns.id,
64+
hostname: `${instance.slug}.${INSTANCE_DOMAIN}`,
65+
})
66+
} else {
67+
console.error('DNS record creation returned failure for', instance.slug)
68+
}
69+
} catch (err) {
70+
console.error('DNS record creation failed:', err)
71+
}
72+
}
73+
5674
const { error: updateError } = await supabaseAdmin
5775
.from('instances')
5876
.update({
5977
hetzner_server_id: server.id,
6078
hetzner_server_type: plan.hetzner_type,
6179
ip_address: server.public_net.ipv4.ip,
6280
gateway_token: gatewayToken,
63-
dashboard_url: dashboardUrl,
81+
dashboard_url: dnsSuccess ? dashboardUrl : null,
6482
})
6583
.eq('id', instance.id)
6684

@@ -69,20 +87,6 @@ export async function provisionInstance(
6987
throw new Error(`Instance DB update failed: ${updateError.message}`)
7088
}
7189

72-
if (isDnsConfigured()) {
73-
try {
74-
const dns = await createDnsRecord(instance.slug, server.public_net.ipv4.ip)
75-
if (dns.success) {
76-
await logInstanceEvent(instance.id, 'dns_created', {
77-
record_id: dns.id,
78-
hostname: `${instance.slug}.${INSTANCE_DOMAIN}`,
79-
})
80-
}
81-
} catch (err) {
82-
console.error('DNS record creation failed:', err)
83-
}
84-
}
85-
8690
await logInstanceEvent(instance.id, 'server_created', {
8791
server_id: server.id,
8892
ip: server.public_net.ipv4.ip,

0 commit comments

Comments
 (0)