diff --git a/Data/Engine/web-interface/src/Devices/ReverseTunnel/Powershell.jsx b/Data/Engine/web-interface/src/Devices/ReverseTunnel/Powershell.jsx
index 927be1ab..bae7b7d5 100644
--- a/Data/Engine/web-interface/src/Devices/ReverseTunnel/Powershell.jsx
+++ b/Data/Engine/web-interface/src/Devices/ReverseTunnel/Powershell.jsx
@@ -27,6 +27,16 @@ import "prismjs/components/prism-powershell";
import "prismjs/themes/prism-okaidia.css";
import Editor from "react-simple-code-editor";
+// Console diagnostics for troubleshooting the connect/disconnect flow.
+const debugLog = (...args) => {
+ try {
+ // eslint-disable-next-line no-console
+ console.error("[ReverseTunnel][PS]", ...args);
+ } catch {
+ // ignore
+ }
+};
+
const MAGIC_UI = {
panelBg: "rgba(7,11,24,0.92)",
panelBorder: "rgba(148, 163, 184, 0.35)",
@@ -108,6 +118,14 @@ export default function ReverseTunnelPowershell({ device }) {
const [copyFlash, setCopyFlash] = useState(false);
const [, setPolling] = useState(false);
const [psStatus, setPsStatus] = useState({});
+ const [milestones, setMilestones] = useState({
+ requested: false,
+ leaseIssued: false,
+ operatorJoined: false,
+ channelOpened: false,
+ ack: false,
+ active: false,
+ });
const socketRef = useRef(null);
const pollTimerRef = useRef(null);
const resizeTimerRef = useRef(null);
@@ -116,6 +134,18 @@ export default function ReverseTunnelPowershell({ device }) {
const joinAttemptsRef = useRef(0);
const DOMAIN_REMOTE_SHELL = "remote-interactive-shell";
+ useEffect(() => {
+ debugLog("component mount", { hostname: device?.hostname, agentId });
+ return () => debugLog("component unmount");
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ debugLog("component mount", { hostname: device?.hostname, agentId });
+ return () => debugLog("component unmount");
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
const hostname = useMemo(() => {
return (
normalizeText(device?.hostname) ||
@@ -139,6 +169,7 @@ export default function ReverseTunnelPowershell({ device }) {
}, [device]);
const resetState = useCallback(() => {
+ debugLog("resetState invoked");
setTunnel(null);
setSessionState("idle");
setStatusMessage("");
@@ -146,6 +177,14 @@ export default function ReverseTunnelPowershell({ device }) {
setOutput("");
setInput("");
setPsStatus({});
+ setMilestones({
+ requested: false,
+ leaseIssued: false,
+ operatorJoined: false,
+ channelOpened: false,
+ ack: false,
+ active: false,
+ });
}, []);
const disconnectSocket = useCallback(() => {
@@ -184,6 +223,7 @@ export default function ReverseTunnelPowershell({ device }) {
useEffect(() => {
return () => {
+ debugLog("cleanup on unmount", { tunnelId: tunnel?.tunnel_id });
stopPolling();
disconnectSocket();
if (joinRetryRef.current) {
@@ -257,10 +297,12 @@ export default function ReverseTunnelPowershell({ device }) {
const pollLoop = useCallback(
(socket, tunnelId) => {
if (!socket || !tunnelId) return;
+ debugLog("pollLoop tick", { tunnelId });
setPolling(true);
pollTimerRef.current = setTimeout(async () => {
const resp = await emitAsync(socket, "ps_poll", {});
if (resp?.error) {
+ debugLog("pollLoop error", resp);
stopPolling();
disconnectSocket();
setPsStatus({});
@@ -273,6 +315,7 @@ export default function ReverseTunnelPowershell({ device }) {
}
if (resp?.status) {
setPsStatus(resp.status);
+ debugLog("pollLoop status", resp.status);
if (resp.status.closed) {
setSessionState("closed");
setTunnel(null);
@@ -281,6 +324,7 @@ export default function ReverseTunnelPowershell({ device }) {
}
if (resp.status.ack) {
setSessionState("connected");
+ setMilestones((prev) => ({ ...prev, ack: true, active: true }));
}
}
pollLoop(socket, tunnelId);
@@ -291,6 +335,7 @@ export default function ReverseTunnelPowershell({ device }) {
const handleDisconnect = useCallback(
async (reason = "operator_disconnect") => {
+ debugLog("handleDisconnect begin", { reason, tunnelId: tunnel?.tunnel_id, psStatus, sessionState });
const socket = socketRef.current;
const tunnelId = tunnel?.tunnel_id;
if (joinRetryRef.current) {
@@ -300,13 +345,17 @@ export default function ReverseTunnelPowershell({ device }) {
joinAttemptsRef.current = 0;
if (socket && tunnelId) {
const frame = buildCloseFrame(1, CLOSE_AGENT_SHUTDOWN, "operator_close");
+ debugLog("emit CLOSE", { tunnelId });
socket.emit("send", { frame });
}
await stopTunnel(reason);
+ debugLog("stopTunnel issued", { tunnelId });
stopPolling();
disconnectSocket();
setTunnel(null);
setSessionState("closed");
+ setMilestones((prev) => ({ ...prev, active: false }));
+ debugLog("handleDisconnect finished", { tunnelId });
},
[disconnectSocket, stopPolling, stopTunnel, tunnel?.tunnel_id]
);
@@ -352,6 +401,7 @@ export default function ReverseTunnelPowershell({ device }) {
socketRef.current = socket;
socket.on("connect_error", () => {
+ debugLog("socket connect_error");
setStatusSeverity("warning");
setStatusMessage("Tunnel namespace unavailable.");
setTunnel(null);
@@ -359,6 +409,7 @@ export default function ReverseTunnelPowershell({ device }) {
});
socket.on("disconnect", () => {
+ debugLog("socket disconnect", { tunnelId: tunnel?.tunnel_id });
stopPolling();
if (sessionState !== "closed") {
setSessionState("disconnected");
@@ -369,6 +420,8 @@ export default function ReverseTunnelPowershell({ device }) {
});
socket.on("connect", async () => {
+ debugLog("socket connect", { tunnelId: lease.tunnel_id });
+ setMilestones((prev) => ({ ...prev, operatorJoined: true }));
setStatusSeverity("info");
setStatusMessage("Joining tunnel...");
const joinResp = await emitAsync(socket, "join", { tunnel_id: lease.tunnel_id });
@@ -388,6 +441,7 @@ export default function ReverseTunnelPowershell({ device }) {
setStatusMessage("Agent did not attach to tunnel (timeout). Try Connect again.");
}
} else {
+ debugLog("join error", joinResp);
setSessionState("error");
setStatusSeverity("error");
setStatusMessage(joinResp.error);
@@ -395,6 +449,8 @@ export default function ReverseTunnelPowershell({ device }) {
return;
}
const dims = measureTerminal();
+ debugLog("ps_open emit", { tunnelId: lease.tunnel_id, dims });
+ setMilestones((prev) => ({ ...prev, channelOpened: true }));
const openResp = await emitAsync(socket, "ps_open", dims);
if (openResp?.error && openResp.error === "ps_unsupported") {
// Suppress warming message; channel will settle once agent attaches.
@@ -415,6 +471,7 @@ export default function ReverseTunnelPowershell({ device }) {
connectSocket(tunnel);
return;
}
+ debugLog("requestTunnel", { agentId, connectionType });
if (!agentId) {
setStatusSeverity("warning");
setStatusMessage("Agent ID is required to request a tunnel.");
@@ -443,6 +500,7 @@ export default function ReverseTunnelPowershell({ device }) {
setStatusMessage("");
return;
}
+ setMilestones((prev) => ({ ...prev, requested: true, leaseIssued: true }));
setTunnel(data);
setStatusMessage("");
setSessionState("lease_issued");
@@ -491,11 +549,6 @@ export default function ReverseTunnelPowershell({ device }) {
}, [stopTunnel, tunnel?.tunnel_id]);
const sessionChips = [
- {
- label: isConnected ? "Connected" : isClosed ? "Session ended" : sessionState === "idle" ? "Idle" : sessionState.replace(/_/g, " "),
- color: isConnected ? MAGIC_UI.accentC : isClosed ? MAGIC_UI.accentB : MAGIC_UI.accentA,
- icon: ,
- },
tunnel?.tunnel_id
? {
label: `Tunnel ${tunnel.tunnel_id.slice(0, 8)}`,
@@ -534,28 +587,6 @@ export default function ReverseTunnelPowershell({ device }) {
Remote Shell
- {hostname ? (
-
- ) : null}
- {agentId ? (
-
- ) : null}
@@ -594,21 +625,53 @@ export default function ReverseTunnelPowershell({ device }) {
-
- {sessionChips.map((chip) => (
-
- ))}
-
+ {/* Workflow pills */}
+
+ {[
+ { key: "requested", label: "Request Tunnel" },
+ {
+ key: "leaseIssued",
+ label: tunnel?.tunnel_id ? `Tunnel ${tunnel.tunnel_id.slice(0, 8)} @ Port ${tunnel.port || "-"}` : "Lease Issued",
+ },
+ { key: "operatorJoined", label: "Operator Joined" },
+ { key: "channelOpened", label: "Open PS Channel" },
+ { key: "ack", label: "Shell ACK" },
+ { key: "active", label: "Session Active" },
+ ].map((step, idx, arr) => {
+ const firstPendingIdx = arr.findIndex((s) => !milestones[s.key]);
+ const status = milestones[step.key]
+ ? "done"
+ : idx === firstPendingIdx && sessionState !== "idle"
+ ? "active"
+ : "idle";
+ const palette =
+ status === "done"
+ ? { bg: "rgba(52,211,153,0.15)", border: "#34d399", color: "#34d399" }
+ : status === "active"
+ ? { bg: "rgba(125,211,252,0.18)", border: MAGIC_UI.accentA, color: MAGIC_UI.accentA }
+ : { bg: "rgba(148,163,184,0.12)", border: `${MAGIC_UI.panelBorder}`, color: MAGIC_UI.textMuted };
+ return (
+
+ : }
+ sx={{
+ backgroundColor: palette.bg,
+ color: palette.color,
+ border: `1px solid ${palette.border}`,
+ fontWeight: 600,
+ ".MuiChip-icon": { color: palette.color },
+ }}
+ />
+ {idx < arr.length - 1 ? (
+
+ →
+
+ ) : null}
+
+ );
+ })}
+