import { useState, type ReactNode } from "react";
import {
AlertTriangle,
Eye,
EyeOff,
Monitor,
MousePointerClick,
Plug,
ShieldCheck,
Wifi,
Wrench,
} from "lucide-react";
import { useVncConsole, type VncConnectionState } from "../hooks/useVncConsole";
type ConsolePageProps = {
theme: "dark" | "light";
};
const RADIUS = "rounded-[5px]";
export function ConsolePage({ theme }: ConsolePageProps) {
const isDark = theme === "dark";
const vnc = useVncConsole({
websocketUrl: "ws://localhost:18450/ws/vnc",
defaultHost: "198.19.0.176",
defaultPort: 5900,
});
const [passwordVisible, setPasswordVisible] = useState(false);
const connectionLabel = getConnectionLabel(vnc.state);
const hasFrame = vnc.frameSize.width > 0 && vnc.frameSize.height > 0;
const frameAspectRatio = hasFrame
? `${vnc.frameSize.width} / ${vnc.frameSize.height}`
: "800 / 480";
return (
ACESSO REMOTO
Controlador
}
title="Estado da sessão"
value={connectionLabel}
color={vnc.connected ? "green" : vnc.connecting ? "blue" : "purple"}
/>
{
const parsed = Number(value);
vnc.setPort(Number.isFinite(parsed) ? parsed : 5900);
}}
disabled={vnc.connecting || vnc.connected}
/>
setPasswordVisible((current) => !current)}
disabled={vnc.connecting || vnc.connected}
className={
isDark
? "grid h-8 w-8 place-items-center rounded-[5px] text-slate-400 transition hover:bg-white/5 hover:text-slate-100 disabled:cursor-not-allowed disabled:opacity-40"
: "grid h-8 w-8 place-items-center rounded-[5px] text-slate-500 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-40"
}
aria-label={passwordVisible ? "Esconder password" : "Mostrar password"}
>
{passwordVisible ? (
) : (
)}
}
/>
(vnc.connected ? vnc.disconnect() : vnc.connect())}
className="mt-5 flex h-11 w-full cursor-pointer items-center justify-center gap-2 rounded-[5px] bg-[#4FD1C5] text-sm font-black text-[#031014] transition hover:bg-[#5FE1D5] disabled:cursor-not-allowed disabled:opacity-50"
>
{vnc.connected ? "Desligar sessão" : vnc.connecting ? "A ligar..." : "Iniciar sessão"}
{vnc.error && (
)}
} label="Modo" value="LAN direta" />
} label="Controlador" value={vnc.host} />
} label="Interação" value="Cliques ativos" />
{!hasFrame && (
{vnc.state === "DISCONNECTED" ? "Sessão terminada" : "Consola indisponível"}
{vnc.state === "DISCONNECTED"
? "Inicie uma nova sessão para voltar a aceder ao controlador."
: "Inicie uma sessão VNC para visualizar e controlar o controlador."}
)}
event.preventDefault()}
className={
hasFrame
? "flex h-full w-full cursor-crosshair items-center justify-center overflow-hidden bg-black"
: "pointer-events-none absolute h-px w-px overflow-hidden opacity-0"
}
>
);
}
function panelClass(isDark: boolean) {
return isDark
? `${RADIUS} flex h-full min-h-0 flex-col border border-white/10 bg-[#071421] p-3 shadow-[0_14px_34px_rgba(0,0,0,0.22)]`
: `${RADIUS} flex h-full min-h-0 flex-col border border-slate-200 bg-white p-3 shadow-[0_10px_26px_rgba(15,23,42,0.06)]`;
}
function panelTitleClass(isDark: boolean) {
return isDark
? "text-base font-black text-slate-100"
: "text-base font-black text-slate-950";
}
function getConnectionLabel(state: VncConnectionState) {
switch (state) {
case "CONNECTING_WS":
return "A ligar WS";
case "CONNECTING_VNC":
return "A ligar VNC";
case "CONNECTED":
return "Ligado";
case "FIRST_FRAME":
return "Ativo";
case "DISCONNECTED":
return "Desconectado";
case "ERROR":
return "Erro";
case "IDLE":
default:
return "A aguardar...";
}
}
function Field({
theme,
label,
value,
onChange,
type = "text",
disabled,
rightElement,
}: {
theme: "dark" | "light";
label: string;
value: string;
onChange: (value: string) => void;
type?: string;
disabled?: boolean;
rightElement?: ReactNode;
}) {
const isDark = theme === "dark";
return (
{label}
onChange(event.target.value)}
className={
isDark
? `${RADIUS} h-10 w-full border border-[#263247] bg-[#07101B] px-3 ${rightElement ? "pr-11" : ""} text-sm font-bold text-white outline-none transition placeholder:text-[#526074] focus:border-[#4FD1C5] disabled:cursor-not-allowed disabled:opacity-60`
: `${RADIUS} h-10 w-full border border-slate-200 bg-white px-3 ${rightElement ? "pr-11" : ""} text-sm font-bold text-[#0F172A] outline-none transition placeholder:text-slate-400 focus:border-[#0F766E] disabled:cursor-not-allowed disabled:opacity-60`
}
/>
{rightElement && (
{rightElement}
)}
);
}
function StatusCard({
theme,
icon,
title,
value,
color,
}: {
theme: "dark" | "light";
icon: ReactNode;
title: string;
value: string;
color: "green" | "blue" | "purple";
}) {
const isDark = theme === "dark";
const colors = {
green: isDark
? "bg-[#13202F] text-[#4FD1C5]"
: "bg-[#ECFDF5] text-[#0F766E]",
blue: isDark
? "bg-[#13202F] text-[#7DD3FC]"
: "bg-[#EFF6FF] text-[#0369A1]",
purple: isDark
? "bg-[#171B2B] text-[#A5B4FC]"
: "bg-[#EEF2FF] text-[#4F46E5]",
};
return (
);
}
function SmallInfo({
theme,
icon,
label,
value,
}: {
theme: "dark" | "light";
icon: ReactNode;
label: string;
value: string;
}) {
const isDark = theme === "dark";
return (
);
}
export default ConsolePage;