Corrige VNC remoto: proxy via Next.js e fix de hidratação SSR

---

- Adicionado docker/server.mjs, entry point customizado que monkey-patcha http.createServer antes do Next.js standalone subir, injetando handler de upgrade para fazer pipe TCP de WebSocket (ws://host:3000/websockify?token=...) direto para localhost:6080;
- Adicionado src/app/api/novnc/[...path]/route.ts, proxy HTTP dos assets estáticos do noVNC (vnc.html, JS, CSS) para localhost:6080, seguindo o mesmo padrão do proxy HLS;
- Corrigido bug de hidratação SSR em src/app/vnc/[id]/page.tsx: URL do iframe agora é computada apenas client-side via useEffect/useState, usando path relativo /api/novnc/ em vez de http://host:6080/;
- Atualizado config/supervisord.conf para iniciar Next.js com node /opt/server.mjs;
- Atualizado docker/Dockerfile para copiar docker/server.mjs para /opt/server.mjs;

---
This commit is contained in:
2026-04-27 09:18:39 -03:00
parent c5d4eadada
commit 1a164f9138
5 changed files with 74 additions and 5 deletions
+27
View File
@@ -0,0 +1,27 @@
import { type NextRequest, NextResponse } from "next/server"
type Ctx = { params: Promise<{ path: string[] }> }
export async function GET(req: NextRequest, { params }: Ctx) {
const { path } = await params
const searchParams = req.nextUrl.searchParams.toString()
const query = searchParams ? `?${searchParams}` : ""
const upstream = `http://localhost:6080/${path.join("/")}${query}`
try {
const res = await fetch(upstream, {
headers: { Accept: req.headers.get("Accept") ?? "*/*" },
})
if (!res.ok) return new NextResponse(null, { status: res.status })
const headers = new Headers()
const ct = res.headers.get("content-type")
if (ct) headers.set("content-type", ct)
headers.set("cache-control", "no-cache")
return new NextResponse(res.body, { status: 200, headers })
} catch {
return new NextResponse(null, { status: 502 })
}
}
+7 -4
View File
@@ -38,14 +38,17 @@ function BackButton({ onClick }: { onClick: () => void }) {
function VncInner() {
const { id } = useParams<{ id: string }>()
const router = useRouter()
const host = typeof window !== "undefined" ? window.location.hostname : "localhost"
const token = encodeURIComponent(`token=${id}`)
const vncUrl = `http://${host}:6080/vnc.html?autoconnect=true&path=websockify%3F${token}`
const [vncUrl, setVncUrl] = useState<string | null>(null)
useEffect(() => {
const token = encodeURIComponent(`token=${id}`)
setVncUrl(`/api/novnc/vnc.html?autoconnect=true&path=websockify%3F${token}`)
}, [id])
return (
<div className="relative bg-black w-screen h-screen overflow-hidden">
<BackButton onClick={() => router.push("/")} />
<iframe src={vncUrl} className="w-screen h-screen border-0" allowFullScreen />
{vncUrl && <iframe src={vncUrl} className="w-screen h-screen border-0" allowFullScreen />}
</div>
)
}