Flag active VNC sessions on the main screen

This commit is contained in:
Viktor Liu
2026-07-30 14:38:54 +02:00
parent 8e919b4ee9
commit abd5022e18
4 changed files with 92 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
import { useTranslation } from "react-i18next";
import { CircleAlert } from "lucide-react";
import { Tooltip } from "@/components/Tooltip";
import { useStatus } from "@/contexts/StatusContext.tsx";
import { cn } from "@/lib/cn.ts";
import { type ReactNode } from "react";
// ActiveSessionIndicator marks that someone is attached to this machine over
// VNC right now, and warns that disconnecting ends that session — which may be
// the session the person reading it is using.
//
// An alert glyph rather than an info one: this is not neutral context, it is a
// state that changes what the disconnect button does to you.
//
// Only a warning, never a block. Input injected by the VNC agent is
// indistinguishable from local input, so we cannot tell whether this UI is being
// driven remotely, and the owner may want to disconnect on purpose either way.
//
// Renders nothing when no session is attached.
export function ActiveSessionIndicator({ className }: { className?: string }): ReactNode {
const { t } = useTranslation();
const { status } = useStatus();
const sessions = status?.vncSessions ?? [];
if (sessions.length === 0) return null;
// The initiator is the dashboard user who started the session, which is more
// use than a source address. Absent when the session carries no identity.
const who = sessions
.map((s) => s.initiator)
.filter((name): name is string => !!name)
.join(", ");
const message = who
? t("connect.activeSession.tooltipNamed", { sessionCount: sessions.length, who })
: t("connect.activeSession.tooltip", { sessionCount: sessions.length });
return (
<Tooltip content={<span className={"block max-w-64"}>{message}</span>}>
<span
role={"status"}
aria-label={message}
className={cn(
"inline-flex items-center gap-1 rounded-full border border-yellow-700/50 bg-yellow-900/20 px-2 py-0.5 text-yellow-300",
className,
)}
>
<CircleAlert size={12} aria-hidden={true} />
<span className={"text-[0.7rem] leading-none"}>
{t("connect.activeSession.badge")}
</span>
</span>
</Tooltip>
);
}

View File

@@ -20,6 +20,7 @@ import { useFocusVisible } from "@/hooks/useFocusVisible";
import { Check as CheckIcon, ChevronDownIcon, Copy as CopyIcon } from "lucide-react";
import * as Popover from "@radix-ui/react-popover";
import netbirdFullLogo from "@/assets/logos/netbird-full.svg";
import { ActiveSessionIndicator } from "@/modules/main/ActiveSessionIndicator.tsx";
enum ConnectionState {
Disconnected = "disconnected",
@@ -272,6 +273,9 @@ export const MainConnectionStatusSwitch = () => {
/>
</CopyToClipboard>
<LocalIpLine ip={ip} ipv6={ipv6} show={show} />
{connState === ConnectionState.Connected && (
<ActiveSessionIndicator className={"mt-3"} />
)}
</div>
</div>
);

View File

@@ -1855,6 +1855,18 @@
"message": "Operation failed.",
"description": "Generic fallback error message used when no specific error applies."
},
"connect.activeSession.badge": {
"message": "Screen shared",
"description": "Short label on the badge shown on the main screen while somebody is attached to this machine over VNC."
},
"connect.activeSession.tooltip": {
"message": "This screen is being viewed over VNC ({sessionCount} session(s)). Disconnecting ends it, and if you are connected through VNC you will lose access.",
"description": "Tooltip on the screen-shared badge. {sessionCount} is how many VNC sessions are attached; wording covers any number since the bundle has no plural forms."
},
"connect.activeSession.tooltipNamed": {
"message": "This screen is being viewed over VNC by {who} ({sessionCount} session(s)). Disconnecting ends it, and if you are connected through VNC you will lose access.",
"description": "As connect.activeSession.tooltip, with {who} naming the dashboard users who started the sessions."
},
"settings.privilege.hint": {
"message": "Requires {actor}. Run this instead:",
"description": "Help text under a remote-access setting the user cannot change: it needs elevated privileges. {actor} is 'root' on Linux/macOS or 'administrator privileges' on Windows. Followed by a copyable command."

View File

@@ -106,6 +106,18 @@ type LocalPeer struct {
Networks []string `json:"networks"`
}
// VNCSession describes one VNC connection currently attached to this machine.
// Surfaced so the UI can warn before an action ends a session that may be the
// one the person is using: input injected by the VNC agent is indistinguishable
// from local input, so the UI cannot tell whether it is being driven remotely.
type VNCSession struct {
RemoteAddress string `json:"remoteAddress"`
Mode string `json:"mode"`
// Initiator is the display name of the dashboard user who started the
// session, when known.
Initiator string `json:"initiator"`
}
// Status is the snapshot the frontend renders on the dashboard.
type Status struct {
Status string `json:"status"`
@@ -122,6 +134,8 @@ type Status struct {
// SessionExpiresAt is the absolute UTC instant the SSO session expires; nil
// when the peer is not SSO-tracked or login expiration is disabled.
SessionExpiresAt *time.Time `json:"sessionExpiresAt,omitempty"`
// VNCSessions lists the VNC connections currently attached to this machine.
VNCSessions []VNCSession `json:"vncSessions,omitempty"`
}
// DaemonFeed fans the daemon's two long-running gRPC streams (SubscribeStatus,
@@ -524,6 +538,14 @@ func statusFromProto(resp *proto.StatusResponse) Status {
},
}
for _, v := range full.GetVncServerState().GetSessions() {
st.VNCSessions = append(st.VNCSessions, VNCSession{
RemoteAddress: v.GetRemoteAddress(),
Mode: v.GetMode(),
Initiator: v.GetInitiator(),
})
}
for _, p := range full.GetPeers() {
st.Peers = append(st.Peers, PeerStatus{
IP: p.GetIP(),