Skip to content

Add embedded VNC viewer - #643

Open
lixmal wants to merge 6 commits into
mainfrom
embedded-vnc
Open

Add embedded VNC viewer#643
lixmal wants to merge 6 commits into
mainfrom
embedded-vnc

Conversation

@lixmal

@lixmal lixmal commented May 17, 2026

Copy link
Copy Markdown
Contributor

Adds a browser VNC viewer for peers, mirroring the existing SSH/RDP integration. Traffic is tunneled through the NetBird WASM client, so no extra ports are exposed to the browser.

  • Add /peer/vnc page wired into the peer overview and dropdown alongside SSH/RDP
  • Embed noVNC via the @novnc/novnc npm package and route its WebSocket through a vnc.proxy.local interceptor that bridges to the Go WASM proxy
  • Support both "attach to current session" and "user session" modes, with a setup screen for Linux/FreeBSD peers
  • Draggable toolbar with Ctrl+Alt+Del and a Paste action that types the host clipboard on the remote (works on login screens via a NetBird-specific RFB message)
  • Two-way clipboard sync on window focus and on remote CutText
  • Map server-side reject codes to user-facing error messages and bounce session-mode failures back to the setup form

Before merge

  • Bump the pinned WASM client version in src/utils/config.ts (wasmPath default, currently https://pkgs.netbird.io/wasm/client/v0.74.2) to a build that carries the embedded VNC server. Has to wait until the netbird client PR is merged and a matching WASM client is published, otherwise the viewer talks to a client that cannot serve VNC.

Issue ticket number and link

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Docs will follow once the feature is enabled by default.

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

Summary by CodeRabbit

  • New Features
    • Added browser-based VNC access from peer dashboards with session controls, view-only mode, clipboard support, cursor settings, and Ctrl+Alt+Del.
    • Added permission-aware VNC availability guidance and connection settings persistence.
    • Added IPv4/IPv6 selection for SSH and RDP connections.
  • Bug Fixes
    • Improved VNC cursor rendering, reconnect flows, and session cleanup.
    • Corrected SSH host selection and RDP IP version handling.

@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

VNC remote access now uses noVNC over a NetBird WebSocket proxy. The change adds VNC controls, session lifecycle handling, query restoration, cursor and clipboard support, and explicit IPv4/IPv6 selection for SSH and RDP.

Changes

VNC Remote Access Integration

Layer / File(s) Summary
VNC contracts and query resolution
src/interfaces/Peer.ts, src/modules/remote-access/vnc/useVNCQueryParams.ts
Adds VNC capability data and resolves VNC session parameters from URL or stored query data.
VNC WebSocket transport
package.json, src/modules/remote-access/vnc/novnc.d.ts, src/modules/remote-access/vnc/websocket-proxy.ts
Adds noVNC and bridges VNC proxy WebSocket traffic.
Temporary NetBird VNC tunnel
src/modules/remote-access/useNetBirdClient.ts
Generates optional VNC session keys and returns structured connection data.
VNC connection hook
src/modules/remote-access/vnc/useVNC.ts
Manages noVNC lifecycle, errors, clipboard, controls, cursor state, resizing, and cleanup.
VNC session UI and controls
src/app/(remote-access)/peer/vnc/page.tsx, src/modules/remote-access/vnc/*, src/app/(dashboard)/peer/page.tsx, src/modules/peers/PeerActionCell.tsx, src/app/globals.css, src/components/ui/FullScreenLoading.tsx
Adds VNC entry points, tooltips, toolbar disconnect handling, cursor styling, and loading labels.
Cross-route query and IP selection
src/auth/SecureProvider.tsx, src/modules/remote-access/{IPVersionSelect.tsx,ssh/*,rdp/*}, src/app/(remote-access)/peer/{ssh,rdp}/page.tsx
Preserves query parameters and adds explicit IPv4/IPv6 selection to SSH and RDP.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant VNCButton
  participant VNCPage
  participant NetBird
  participant useVNC
  participant noVNC

  User->>VNCButton: Open VNC
  VNCButton->>VNCPage: Open peer VNC page
  VNCPage->>NetBird: Establish temporary tunnel
  VNCPage->>useVNC: Start VNC connection
  useVNC->>NetBird: Create VNC proxy
  NetBird-->>useVNC: Return proxy URL
  useVNC->>noVNC: Initialize RFB
  noVNC-->>VNCPage: Report connection status
Loading

Poem

A rabbit opens VNC with care,
Through NetBird tunnels in the air.
IPv6 joins the remote screen,
While noVNC keeps cursors clean.
The toolbar ends the session bright—
Then hops away into the night.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: adding an embedded VNC viewer.
Description check ✅ Passed The description covers the implementation, scope, documentation decision, and pending WASM update; the issue ticket section remains empty.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch embedded-vnc

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lixmal lixmal changed the title [client] Add embedded VNC viewer Add embedded VNC viewer May 17, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (3)
src/modules/remote-access/vnc/useVNCQueryParams.ts (1)

59-59: 💤 Low value

Use the useLocalStorage hook consistently.

Line 59 directly calls localStorage.getItem, but the hook already imports and uses useLocalStorage (line 35). For consistency and testability, prefer using the hook's getter or a ref pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/vnc/useVNCQueryParams.ts` at line 59, The line
directly calling localStorage.getItem should be replaced to use the
already-imported useLocalStorage hook for consistency and testability: inside
the useVNCQueryParams function, obtain the stored value via the hook's API
(e.g., call useLocalStorage('netbird-query-params') or use the hook's provided
getter/ref) instead of localStorage.getItem and assign that result to the
storedParams variable; update any subsequent logic that reads storedParams to
use the hook-returned value.
src/modules/remote-access/vnc/VNCButton.tsx (1)

33-39: ⚡ Quick win

Consider handling popup blocker scenarios.

window.open can return null if blocked by popup blockers. Consider checking the return value and notifying the user if the window fails to open.

💡 Example implementation
   const openVNCPage = () => {
-    window.open(
+    const newWindow = window.open(
       `/peer/vnc?id=${peer.id}`,
       "_blank",
       "noopener,noreferrer,width=1200,height=800,left=100,top=100,location=no,toolbar=no,menubar=no,status=no",
     );
+    if (!newWindow) {
+      notify({
+        title: "Popup Blocked",
+        description: "Please allow popups for this site to open VNC sessions.",
+        type: "error",
+      });
+    }
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/vnc/VNCButton.tsx` around lines 33 - 39,
openVNCPage uses window.open to open the VNC page but doesn't handle popup
blockers; modify the openVNCPage function to capture the return value of
window.open (called with `peer.id`) and check for null/undefined, and if it
failed, surface a user-facing notification (e.g., alert/toast via the app's
notification system) and optionally log the failure with context including the
peer.id so users/developers know the popup was blocked.
src/modules/remote-access/vnc/useVNC.ts (1)

274-279: ⚡ Quick win

Avoid relying on noVNC's private _updateScale API.

The _updateScale method is internal to noVNC and undocumented; calling it directly risks breakage on upgrades. According to noVNC's public API, viewport scaling should be handled automatically when scaleViewport is true. If resizing doesn't rescale the viewport correctly, first ensure the VNC container has explicit height/width set. If the issue persists, consider reporting it as a bug to the noVNC project rather than working around it with private APIs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/vnc/useVNC.ts` around lines 274 - 279, The
handleResize debounce currently calls noVNC's private _updateScale on
rfbRef.current which is unsafe; remove the direct call to
rfbRef.current._updateScale and rely on the public behavior when
rfbRef.current?.scaleViewport is true. Instead ensure the VNC
container/component that handleResize acts on has explicit width/height in CSS
or inline style so noVNC can recalculate scale automatically; if automatic
scaling still fails, revert the workaround and open an upstream issue with the
noVNC project. Update the handleResize function and any related resize logic
(rfbRef, scaleViewport, and the VNC container setup) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Line 21: The package.json currently pins the pre-release dependency string
"`@novnc/novnc`": "^1.7.0" — replace that version with the stable release "^1.6.0"
in package.json (update the dependency entry for "`@novnc/novnc`"), then run your
package manager (npm/yarn/pnpm) to update node_modules and the lockfile so the
lockfile reflects the new stable version.

In `@src/app/`(remote-access)/peer/vnc/page.tsx:
- Line 31: The page currently calls useVNCQueryParams and destructures peerId
which, if missing, keeps fetch disabled and leaves FullScreenLoading forever;
update the VNC page component to explicitly detect when peerId is falsy and
handle it (e.g., render an error/validation UI or redirect) instead of rendering
FullScreenLoading with no recovery; locate useVNCQueryParams usage and the
FullScreenLoading render path in the VNC page (references: peerId,
useVNCQueryParams, FullScreenLoading) and add a clear early-return branch that
stops any data fetching and shows the error state so the user can correct or
navigate away (apply same change to the similar logic around lines 40-50).
- Around line 144-147: The catch handler for vnc.connect(...) currently calls
sendErrorNotification and also sets setConnectFailed, which causes the separate
vnc.error effect to emit the same toast; update the logic so only one
notification is sent: either remove sendErrorNotification from the .catch(...)
and rely on the vnc.error effect, or add a short-lived guard flag (e.g., a ref
like suppressVncErrorToast) checked in both places so .catch(...) sets the flag
before calling setConnectFailed and the vnc.error effect skips sending if the
flag is set; apply the same change to the other occurrence around the block that
starts at the second location (lines ~177-180) to prevent duplicate toasts.

In `@src/modules/remote-access/vnc/useVNCQueryParams.ts`:
- Around line 58-81: The restoration logic in useVNCQueryParams currently only
preserves "id" (storedPeerId) and resets everything else to defaultSettings;
instead, parse urlParams (from paramsString) and copy all relevant keys into
newSearchParams and the state passed to setParams so mode, username, quality,
scale, resize, cursor (and id -> peerId) are restored if present, falling back
to defaultSettings for any missing values; then call router.replace with the
full reconstructed query string, call setLocalQueryParams("") (or clear
localStorage) and ensure setParams receives the merged object (e.g., { peerId:
urlParams.get("id"), mode: urlParams.get("mode") || defaultSettings.mode,
username: urlParams.get("username") || "", settings: {
...defaultSettings.settings, quality: urlParams.get("quality") ||
defaultSettings.settings.quality, scale: urlParams.get("scale") ||
defaultSettings.settings.scale, resize: urlParams.get("resize") ||
defaultSettings.settings.resize, cursor: urlParams.get("cursor") ||
defaultSettings.settings.cursor } }) so restored values persist after the auth
redirect.

In `@src/modules/remote-access/vnc/VNCButton.tsx`:
- Around line 33-39: The openVNCPage handler uses a relative URL which can
resolve incorrectly; update the URL in openVNCPage to an absolute path by
prefixing with a slash (e.g., "/peer/vnc?id=" + peer.id or template
`/peer/vnc?id=${peer.id}`) when calling window.open so it always navigates to
the correct route; keep the existing window.open options (target "_blank" and
window features) unchanged.

In `@src/modules/remote-access/vnc/VNCToolbar.tsx`:
- Around line 36-39: The cleanup function currently only removes event listeners
but doesn't reset drag state; update the unmount cleanup in VNCToolbar to also
set draggingRef.current = false and restore document.body.style.userSelect = ""
(or its previous value) so a mid-drag unmount doesn't leave selection disabled;
locate the effect that adds window.addEventListener for "pointermove" and
"pointerup" (handlers onMove and onUp) and extend its return to reset
draggingRef and userSelect alongside removing the listeners.

In `@src/modules/remote-access/vnc/websocket-proxy.ts`:
- Around line 167-171: The construct trap for the WebSocket proxy assumes
args[0] is a string and calls .includes(), which will throw if a URL object is
passed; update the construct function so it normalizes the first argument: if
args[0] is a URL instance use args[0].toString() or args[0].href, otherwise
coerce it to a string, then perform the includes("vnc.proxy.local") check and
return new VNCProxyWebSocket(urlString) cast as WebSocket when matched; keep
existing behavior for non-matching values.

---

Nitpick comments:
In `@src/modules/remote-access/vnc/useVNC.ts`:
- Around line 274-279: The handleResize debounce currently calls noVNC's private
_updateScale on rfbRef.current which is unsafe; remove the direct call to
rfbRef.current._updateScale and rely on the public behavior when
rfbRef.current?.scaleViewport is true. Instead ensure the VNC
container/component that handleResize acts on has explicit width/height in CSS
or inline style so noVNC can recalculate scale automatically; if automatic
scaling still fails, revert the workaround and open an upstream issue with the
noVNC project. Update the handleResize function and any related resize logic
(rfbRef, scaleViewport, and the VNC container setup) accordingly.

In `@src/modules/remote-access/vnc/useVNCQueryParams.ts`:
- Line 59: The line directly calling localStorage.getItem should be replaced to
use the already-imported useLocalStorage hook for consistency and testability:
inside the useVNCQueryParams function, obtain the stored value via the hook's
API (e.g., call useLocalStorage('netbird-query-params') or use the hook's
provided getter/ref) instead of localStorage.getItem and assign that result to
the storedParams variable; update any subsequent logic that reads storedParams
to use the hook-returned value.

In `@src/modules/remote-access/vnc/VNCButton.tsx`:
- Around line 33-39: openVNCPage uses window.open to open the VNC page but
doesn't handle popup blockers; modify the openVNCPage function to capture the
return value of window.open (called with `peer.id`) and check for
null/undefined, and if it failed, surface a user-facing notification (e.g.,
alert/toast via the app's notification system) and optionally log the failure
with context including the peer.id so users/developers know the popup was
blocked.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 479650dc-fc85-49ef-95b5-53bfe9522fda

📥 Commits

Reviewing files that changed from the base of the PR and between 7400ac8 and c2a18d2.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • package.json
  • src/app/(dashboard)/peer/page.tsx
  • src/app/(remote-access)/peer/vnc/page.tsx
  • src/app/globals.css
  • src/auth/SecureProvider.tsx
  • src/interfaces/Peer.ts
  • src/modules/peers/PeerConnectButton.tsx
  • src/modules/remote-access/vnc/VNCButton.tsx
  • src/modules/remote-access/vnc/VNCToolbar.tsx
  • src/modules/remote-access/vnc/VNCTooltip.tsx
  • src/modules/remote-access/vnc/novnc.d.ts
  • src/modules/remote-access/vnc/useVNC.ts
  • src/modules/remote-access/vnc/useVNCQueryParams.ts
  • src/modules/remote-access/vnc/websocket-proxy.ts

Comment thread package.json
Comment thread src/app/(remote-access)/peer/vnc/page.tsx Outdated
Comment thread src/app/(remote-access)/peer/vnc/page.tsx Outdated
Comment thread src/modules/remote-access/vnc/useVNCQueryParams.ts
Comment thread src/modules/remote-access/vnc/VNCButton.tsx
Comment thread src/modules/remote-access/vnc/VNCToolbar.tsx
Comment thread src/modules/remote-access/vnc/websocket-proxy.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/modules/remote-access/vnc/useVNCQueryParams.ts`:
- Line 57: The mode query param is being force-cast without validating its value
in useVNCQueryParams (variable mode and the second occurrence on line 91);
replace the direct type assertion with a validation step: read the raw string
from searchParams.get("mode"), check it against an allowlist like
["attach","session"] (e.g., allowedModes.includes(raw)), and only then
assign/cast to "attach" | "session", otherwise fall back to "attach"; apply the
same validation logic to the other occurrence on line 91 so arbitrary strings
cannot be treated as valid modes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2e375997-c69e-499b-8f68-a1a14d1df03f

📥 Commits

Reviewing files that changed from the base of the PR and between c2a18d2 and 58961d2.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • package.json
  • src/app/(dashboard)/peer/page.tsx
  • src/app/(remote-access)/peer/vnc/page.tsx
  • src/app/globals.css
  • src/auth/SecureProvider.tsx
  • src/interfaces/Peer.ts
  • src/modules/peers/PeerConnectButton.tsx
  • src/modules/remote-access/vnc/VNCButton.tsx
  • src/modules/remote-access/vnc/VNCToolbar.tsx
  • src/modules/remote-access/vnc/VNCTooltip.tsx
  • src/modules/remote-access/vnc/novnc.d.ts
  • src/modules/remote-access/vnc/useVNC.ts
  • src/modules/remote-access/vnc/useVNCQueryParams.ts
  • src/modules/remote-access/vnc/websocket-proxy.ts
✅ Files skipped from review due to trivial changes (3)
  • src/modules/remote-access/vnc/novnc.d.ts
  • src/auth/SecureProvider.tsx
  • src/app/(dashboard)/peer/page.tsx
🚧 Files skipped from review as they are similar to previous changes (10)
  • package.json
  • src/app/globals.css
  • src/interfaces/Peer.ts
  • src/modules/peers/PeerConnectButton.tsx
  • src/modules/remote-access/vnc/VNCTooltip.tsx
  • src/modules/remote-access/vnc/VNCToolbar.tsx
  • src/modules/remote-access/vnc/VNCButton.tsx
  • src/app/(remote-access)/peer/vnc/page.tsx
  • src/modules/remote-access/vnc/websocket-proxy.ts
  • src/modules/remote-access/vnc/useVNC.ts

Comment thread src/modules/remote-access/vnc/useVNCQueryParams.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/app/(remote-access)/peer/vnc/page.tsx (1)

164-177: 💤 Low value

Remove unused sendErrorNotification from dependency array.

sendErrorNotification is included in the effect's dependency array but is never called within the effect body. Removing it clarifies the effect's actual dependencies.

♻️ Suggested fix
   ], [
     client.status,
     vnc.connect,
     vnc.status,
     peer.ip,
     isNetBirdConnecting,
     showSetup,
     mode,
     username,
     accessToken,
     settings,
     connectFailed,
-    sendErrorNotification,
   ]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(remote-access)/peer/vnc/page.tsx around lines 164 - 177, Remove
sendErrorNotification from the useEffect dependency array in the VNC page (the
effect that currently lists client.status, vnc.connect, vnc.status, peer.ip,
isNetBirdConnecting, showSetup, mode, username, accessToken, settings,
connectFailed, sendErrorNotification). Edit the effect where
sendErrorNotification is not referenced in the body and delete it from the
dependencies so the array only includes actual used symbols (client.status,
vnc.connect, vnc.status, peer.ip, isNetBirdConnecting, showSetup, mode,
username, accessToken, settings, connectFailed).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/app/`(remote-access)/peer/vnc/page.tsx:
- Around line 164-177: Remove sendErrorNotification from the useEffect
dependency array in the VNC page (the effect that currently lists client.status,
vnc.connect, vnc.status, peer.ip, isNetBirdConnecting, showSetup, mode,
username, accessToken, settings, connectFailed, sendErrorNotification). Edit the
effect where sendErrorNotification is not referenced in the body and delete it
from the dependencies so the array only includes actual used symbols
(client.status, vnc.connect, vnc.status, peer.ip, isNetBirdConnecting,
showSetup, mode, username, accessToken, settings, connectFailed).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 69b6175f-8535-4c25-a72d-23ff7fafb2b0

📥 Commits

Reviewing files that changed from the base of the PR and between 58961d2 and 8e0d62d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • package.json
  • src/app/(dashboard)/peer/page.tsx
  • src/app/(remote-access)/peer/vnc/page.tsx
  • src/app/globals.css
  • src/auth/SecureProvider.tsx
  • src/interfaces/Peer.ts
  • src/modules/peers/PeerConnectButton.tsx
  • src/modules/remote-access/vnc/VNCButton.tsx
  • src/modules/remote-access/vnc/VNCToolbar.tsx
  • src/modules/remote-access/vnc/VNCTooltip.tsx
  • src/modules/remote-access/vnc/novnc.d.ts
  • src/modules/remote-access/vnc/useVNC.ts
  • src/modules/remote-access/vnc/useVNCQueryParams.ts
  • src/modules/remote-access/vnc/websocket-proxy.ts
✅ Files skipped from review due to trivial changes (1)
  • src/app/globals.css
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/interfaces/Peer.ts
  • src/app/(dashboard)/peer/page.tsx
  • src/modules/remote-access/vnc/novnc.d.ts
  • package.json
  • src/modules/remote-access/vnc/VNCButton.tsx
  • src/modules/remote-access/vnc/useVNC.ts
  • src/modules/remote-access/vnc/VNCToolbar.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/modules/remote-access/useNetBirdClient.ts (1)

303-316: ⚡ Quick win

Guard netbirdGenerateVNCSessionKey() against throwing.

wasmGenerate() is called outside a dedicated try/catch within the wider try. If the WASM helper throws (e.g., key generation fails inside Go), the entire connectTemporary flow aborts before the WireGuard connect() call, even though falling back to a sessionless connect is the documented behavior for older WASM bundles. Wrap the call so a throwing helper degrades to sessionPub = undefined and is logged, rather than failing the temporary-access request.

🛡️ Proposed fix
   const wasmGenerate = (window as any).netbirdGenerateVNCSessionKey;
   if (typeof wasmGenerate === "function") {
-    const sk = wasmGenerate();
-    if (sk && typeof sk === "object" && typeof sk.publicKey === "string") {
-      sessionPub = sk.publicKey;
-      keySessionId = sk.sessionId ?? null;
+    try {
+      const sk = wasmGenerate();
+      if (sk && typeof sk === "object" && typeof sk.publicKey === "string") {
+        sessionPub = sk.publicKey;
+        keySessionId = sk.sessionId ?? null;
+      }
+    } catch (e) {
+      console.warn("netbirdGenerateVNCSessionKey failed:", e);
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/useNetBirdClient.ts` around lines 303 - 316, Wrap
the call to the WASM helper (the wasmGenerate / netbirdGenerateVNCSessionKey
invocation inside connectTemporary) in a try/catch so any thrown errors are
caught and do not abort the flow; on error set sessionPub = undefined and
keySessionId = null, log the exception (with context) and continue so the
fallback sessionless connect path can proceed. Ensure you only change the block
that calls wasmGenerate() and preserve the existing type checks (sk.publicKey)
and behavior when the helper returns normally.
src/modules/remote-access/vnc/useVNC.ts (1)

334-350: 💤 Low value

Style nit: short-circuit expression statement on Line 341.

rfbRef.current?.scaleViewport && rfbRef.current._updateScale?.(); works at runtime, but it's an expression statement using && as a guard, which is awkward to read and is typically flagged by @typescript-eslint/no-unused-expressions. Consider a straightforward if:

♻️ Proposed refactor
-      timeout = setTimeout(() => {
-        rfbRef.current?.scaleViewport && rfbRef.current._updateScale?.();
-      }, 200);
+      timeout = setTimeout(() => {
+        if (rfbRef.current?.scaleViewport) {
+          rfbRef.current._updateScale?.();
+        }
+      }, 200);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/vnc/useVNC.ts` around lines 334 - 350, The
short-circuit expression in the handleResize callback uses
"rfbRef.current?.scaleViewport && rfbRef.current._updateScale?.();" which is an
unused-expression style and should be replaced with an explicit conditional;
update the handleResize function inside the useEffect to first safely grab
rfbRef.current, then if rfbRef.current?.scaleViewport is truthy call
rfbRef.current._updateScale?.(), so replace the && guard with a clear
if-statement referencing rfbRef and _updateScale to satisfy linters and improve
readability.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/`(remote-access)/peer/vnc/page.tsx:
- Around line 37-54: The current render logic can get stuck on FullScreenLoading
when peer fetch fails because peerId is true, peer is undefined and
isPeerLoading is false; update the JSX conditional in the component rendering
(the block that currently chooses between VNCSession, MissingPeerError, and
FullScreenLoading) to explicitly handle the case peerId && !peer &&
!isPeerLoading and render an error state (reuse MissingPeerError or add a new
PeerLoadError component) so failed fetches surface a "peer not found / failed to
load" message instead of an infinite loading spinner; ensure you reference the
same variables (peerId, peer, isPeerLoading, ready) and components (VNCSession,
MissingPeerError, FullScreenLoading) when adjusting the conditional.
- Around line 114-139: The inline comment above the guards in connectNetBird is
misleading: connectTemporary returns a TemporaryConnectResult object on all
paths (with fields possibly null), not null itself; update the comment to state
that the returned object's fields (e.g., targetPubKey and keySessionId) may be
null and that the existing if (result.targetPubKey) / if (result.keySessionId)
checks intentionally avoid overwriting earlier valid values. Reference
connectNetBird, connectTemporary, TemporaryConnectResult, result.targetPubKey
and result.keySessionId when making the change.

In `@src/modules/remote-access/useNetBirdClient.ts`:
- Around line 280-290: The early-return in connectTemporary (checking
netBirdStore.getState().status against NetBirdStatus.CONNECTING/CONNECTED)
currently returns targetPubKey: null and keySessionId: null, which loses
per-VNC-session X25519 metadata; change this path to either (A) remint/refetch
the VNC session key before returning by invoking the same helper used in the
fresh-connect flow so targetPubKey and keySessionId are populated, or (B) return
a discriminated result indicating a reused tunnel (e.g., add a reusedTunnel:
true flag and preserve any existing session metadata from netBirdStore or the
netBird client) so callers can distinguish "reused tunnel" vs "fresh connect"
and act accordingly; update connectTemporary to use one of these approaches and
adjust any callers that depend on targetPubKey/keySessionId (references:
connectTemporary, netBirdStore, NetBirdStatus, targetPubKey, keySessionId).

---

Nitpick comments:
In `@src/modules/remote-access/useNetBirdClient.ts`:
- Around line 303-316: Wrap the call to the WASM helper (the wasmGenerate /
netbirdGenerateVNCSessionKey invocation inside connectTemporary) in a try/catch
so any thrown errors are caught and do not abort the flow; on error set
sessionPub = undefined and keySessionId = null, log the exception (with context)
and continue so the fallback sessionless connect path can proceed. Ensure you
only change the block that calls wasmGenerate() and preserve the existing type
checks (sk.publicKey) and behavior when the helper returns normally.

In `@src/modules/remote-access/vnc/useVNC.ts`:
- Around line 334-350: The short-circuit expression in the handleResize callback
uses "rfbRef.current?.scaleViewport && rfbRef.current._updateScale?.();" which
is an unused-expression style and should be replaced with an explicit
conditional; update the handleResize function inside the useEffect to first
safely grab rfbRef.current, then if rfbRef.current?.scaleViewport is truthy call
rfbRef.current._updateScale?.(), so replace the && guard with a clear
if-statement referencing rfbRef and _updateScale to satisfy linters and improve
readability.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9b67cc0b-2da6-4287-a782-638af298d0fd

📥 Commits

Reviewing files that changed from the base of the PR and between 8e0d62d and 16605ee.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (15)
  • package.json
  • src/app/(dashboard)/peer/page.tsx
  • src/app/(remote-access)/peer/vnc/page.tsx
  • src/app/globals.css
  • src/auth/SecureProvider.tsx
  • src/interfaces/Peer.ts
  • src/modules/peers/PeerConnectButton.tsx
  • src/modules/remote-access/useNetBirdClient.ts
  • src/modules/remote-access/vnc/VNCButton.tsx
  • src/modules/remote-access/vnc/VNCToolbar.tsx
  • src/modules/remote-access/vnc/VNCTooltip.tsx
  • src/modules/remote-access/vnc/novnc.d.ts
  • src/modules/remote-access/vnc/useVNC.ts
  • src/modules/remote-access/vnc/useVNCQueryParams.ts
  • src/modules/remote-access/vnc/websocket-proxy.ts

Comment thread src/app/(remote-access)/peer/vnc/page.tsx
Comment thread src/app/(remote-access)/peer/vnc/page.tsx
Comment thread src/modules/remote-access/useNetBirdClient.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/modules/remote-access/useNetBirdClient.ts (1)

338-340: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing initialize in dependency array.

The callback uses await initialize() at line 306, but initialize is not listed in the dependency array. This could cause stale closure issues if initialize changes identity.

🛡️ Proposed fix
-    [connect, peerRequest],
+    [connect, initialize, peerRequest],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/useNetBirdClient.ts` around lines 338 - 340, The
useCallback that depends on [connect, peerRequest] also calls await
initialize(), which can lead to stale closures; update the dependency array for
the callback to include initialize (so it becomes [connect, peerRequest,
initialize]) to ensure the callback is recreated when initialize's identity
changes; locate the callback in useNetBirdClient (the function that references
connect, peerRequest and calls initialize) and add initialize to its dependency
list.
♻️ Duplicate comments (1)
src/app/(remote-access)/peer/vnc/page.tsx (1)

37-51: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Render a failure state when peer fetch settles without data.

When peerId is present but the fetch resolves with no peer, this branch still falls through to FullScreenLoading indefinitely.

🔧 Suggested fix
   const {
     data: peer,
     isLoading: isPeerLoading,
+    error: peerError,
   } = useFetchApi<Peer>(`/peers/${peerId}`, true, false, !!peerId);
@@
-      ) : ready && !peerId ? (
+      ) : ready && (!peerId || (!!peerId && !peer && !isPeerLoading)) ? (
         <MissingPeerError />
+      ) : peerError ? (
+        <MissingPeerError />
       ) : (
         <FullScreenLoading />
       )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(remote-access)/peer/vnc/page.tsx around lines 37 - 51, The current
JSX conditional falls back to FullScreenLoading when peerId is set but the fetch
finished without returning peer; update the render logic in the component that
returns the VNCSession block so you explicitly handle the "peerId &&
!isPeerLoading && !peer" case and render a failure state (e.g., MissingPeerError
or a new PeerNotFound component) instead of FullScreenLoading; locate the
existing conditional that references peerId, peer, isPeerLoading, ready and
VNCSession and insert a branch before the final loading fallback that returns
the failure component when the fetch has settled with no peer.
🧹 Nitpick comments (1)
src/modules/remote-access/vnc/novnc.d.ts (1)

1-46: noVNC RFB typings are largely consistent with the actual API surface

  • RFBOptions fields credentials, repeaterID, and wsProtocols match noVNC’s documented RFB options, and the internal scaling behavior behind scaleViewport corresponds to RFB.prototype._updateScale.
  • Declared RFB methods (sendCredentials, sendCtrlAltDel, clipboardPasteFrom, machineShutdown/machineReboot/machineReset, etc.) match the documented noVNC RFB API.
  • Optional refactor: _updateScale?() is internal/underscore-prefixed and isn’t referenced anywhere in the repo codebase; consider removing it from src/modules/remote-access/vnc/novnc.d.ts (line ~44) unless you explicitly depend on it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/vnc/novnc.d.ts` around lines 1 - 46, The declared
optional internal method _updateScale?() on the RFB class in
src/modules/remote-access/vnc/novnc.d.ts is an underscore-prefixed internal API
and appears unused; remove the _updateScale?() declaration from the RFB
interface in this file (or, if you do actually rely on it, leave it but add a
comment explaining why) — search for usages of _updateScale in the repo to
confirm it’s unused, then delete the _updateScale?() line from the RFB class
declaration in novnc.d.ts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/`(remote-access)/peer/vnc/page.tsx:
- Around line 131-138: The current catch/finally resets isNetBirdConnecting
immediately which retriggers the effect that calls connectTemporary and causes
rapid retry/toast loops; modify the logic around
connectTemporary/isNetBirdConnecting by adding a cooldown or failure flag (e.g.,
netBirdRetryDisabled or lastNetBirdAttempt timestamp) and check that
flag/timestamp in the effect that invokes connectTemporary to skip retries until
a configurable backoff period has elapsed or until user action; set the flag (or
record timestamp) inside the catch block when sendErrorNotification is called
and only clear it after the backoff expires (or when user retries), ensuring
setIsNetBirdConnecting(false) does not alone cause an immediate re-attempt.

In `@src/auth/SecureProvider.tsx`:
- Around line 32-50: The effect is persisting query params for unauthenticated
users on any path; restrict persistence to preserve routes only by using the
existing PRESERVE_QUERY_PARAMS_PATHS/currentPath check: only run the query-param
capture logic when onPreservePath is true (and the user is unauthenticated),
e.g. move the try/URLSearchParams/VALID_PARAMS/QUERY_PARAMS_KEY block behind an
if(onPreservePath) (or return early when not onPreservePath) so only routes in
PRESERVE_QUERY_PARAMS_PATHS can store params.

In `@src/modules/remote-access/vnc/VNCToolbar.tsx`:
- Around line 21-26: VNCToolbar's localStorage reads/writes (STORAGE_KEY) can
throw in restricted/private storage contexts; wrap both the initial getter in
the useState initializer for xPercent and any subsequent localStorage.setItem
calls (e.g., where setXPercent persists) in safe guards: check typeof window !==
"undefined" and wrap getItem/setItem in try/catch, falling back to default 50 on
read errors and silently skipping writes on errors, so the component continues
to function even if localStorage is unavailable.

---

Outside diff comments:
In `@src/modules/remote-access/useNetBirdClient.ts`:
- Around line 338-340: The useCallback that depends on [connect, peerRequest]
also calls await initialize(), which can lead to stale closures; update the
dependency array for the callback to include initialize (so it becomes [connect,
peerRequest, initialize]) to ensure the callback is recreated when initialize's
identity changes; locate the callback in useNetBirdClient (the function that
references connect, peerRequest and calls initialize) and add initialize to its
dependency list.

---

Duplicate comments:
In `@src/app/`(remote-access)/peer/vnc/page.tsx:
- Around line 37-51: The current JSX conditional falls back to FullScreenLoading
when peerId is set but the fetch finished without returning peer; update the
render logic in the component that returns the VNCSession block so you
explicitly handle the "peerId && !isPeerLoading && !peer" case and render a
failure state (e.g., MissingPeerError or a new PeerNotFound component) instead
of FullScreenLoading; locate the existing conditional that references peerId,
peer, isPeerLoading, ready and VNCSession and insert a branch before the final
loading fallback that returns the failure component when the fetch has settled
with no peer.

---

Nitpick comments:
In `@src/modules/remote-access/vnc/novnc.d.ts`:
- Around line 1-46: The declared optional internal method _updateScale?() on the
RFB class in src/modules/remote-access/vnc/novnc.d.ts is an underscore-prefixed
internal API and appears unused; remove the _updateScale?() declaration from the
RFB interface in this file (or, if you do actually rely on it, leave it but add
a comment explaining why) — search for usages of _updateScale in the repo to
confirm it’s unused, then delete the _updateScale?() line from the RFB class
declaration in novnc.d.ts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2164f85b-7db4-434c-81e2-b92183be16ee

📥 Commits

Reviewing files that changed from the base of the PR and between 16605ee and ac80584.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (16)
  • package.json
  • src/app/(dashboard)/peer/page.tsx
  • src/app/(remote-access)/peer/vnc/page.tsx
  • src/app/globals.css
  • src/auth/SecureProvider.tsx
  • src/components/ui/FullScreenLoading.tsx
  • src/interfaces/Peer.ts
  • src/modules/peers/PeerConnectButton.tsx
  • src/modules/remote-access/useNetBirdClient.ts
  • src/modules/remote-access/vnc/VNCButton.tsx
  • src/modules/remote-access/vnc/VNCToolbar.tsx
  • src/modules/remote-access/vnc/VNCTooltip.tsx
  • src/modules/remote-access/vnc/novnc.d.ts
  • src/modules/remote-access/vnc/useVNC.ts
  • src/modules/remote-access/vnc/useVNCQueryParams.ts
  • src/modules/remote-access/vnc/websocket-proxy.ts

Comment thread src/app/(remote-access)/peer/vnc/page.tsx
Comment thread src/auth/SecureProvider.tsx Outdated
Comment thread src/modules/remote-access/vnc/VNCToolbar.tsx
@lixmal
lixmal force-pushed the embedded-vnc branch 2 times, most recently from 333e16b to b44e0d4 Compare June 10, 2026 10:24
@lixmal
lixmal force-pushed the embedded-vnc branch 2 times, most recently from 17102fd to f5de9d4 Compare July 13, 2026 09:13

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(remote-access)/peer/ssh/page.tsx (1)

181-181: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Loading message shows IPv4 address even when IPv6 is selected.

The LoadingMessage hardcodes peer.ip, so when a user selects IPv6 the connecting text still shows the IPv4 address. Use sshHost for consistency.

✏️ Proposed fix
-        <LoadingMessage message={`Connecting to ${username}@${peer.ip}...`} />
+        <LoadingMessage message={`Connecting to ${username}@${sshHost}...`} />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(remote-access)/peer/ssh/page.tsx at line 181, Update the
LoadingMessage in the SSH connection flow to display sshHost instead of peer.ip,
ensuring the connecting text reflects the selected IPv4 or IPv6 address.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/app/`(remote-access)/peer/ssh/page.tsx:
- Line 181: Update the LoadingMessage in the SSH connection flow to display
sshHost instead of peer.ip, ensuring the connecting text reflects the selected
IPv4 or IPv6 address.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 674a8cab-6b28-4046-8b61-4e1437bafbb9

📥 Commits

Reviewing files that changed from the base of the PR and between 16605ee and f5de9d4.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • package.json
  • src/app/(dashboard)/peer/page.tsx
  • src/app/(remote-access)/peer/rdp/page.tsx
  • src/app/(remote-access)/peer/ssh/page.tsx
  • src/app/(remote-access)/peer/vnc/page.tsx
  • src/app/globals.css
  • src/auth/SecureProvider.tsx
  • src/components/ui/FullScreenLoading.tsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • package.json
  • src/app/(dashboard)/peer/page.tsx
  • src/app/globals.css
  • src/components/ui/FullScreenLoading.tsx
  • src/app/(remote-access)/peer/vnc/page.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/modules/remote-access/vnc/VNCToolbar.tsx`:
- Around line 127-141: Update the toolbar’s hover-expanded container around the
End session button to also expand with group-focus-within:max-h-20, and add
visible focus-visible styling to the button so keyboard users can discover and
activate it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c20aa6f0-98af-433c-bc50-89d590cf5b1b

📥 Commits

Reviewing files that changed from the base of the PR and between 28c2219 and b39b5c6.

📒 Files selected for processing (2)
  • src/app/(remote-access)/peer/vnc/page.tsx
  • src/modules/remote-access/vnc/VNCToolbar.tsx

Comment thread src/modules/remote-access/vnc/VNCToolbar.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
src/modules/remote-access/vnc/useVNC.ts (3)

205-212: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Guard connect against an active session.

The early return only covers VNCStatus.CONNECTING. If a caller invokes connect while the status is CONNECTED, the hook creates a second RFB instance and overwrites rfbRef.current, so the previous session and its WebSocket leak. The page currently guards on VNCStatus.DISCONNECTED, so this is defensive only.

♻️ Proposed guard
-      if (statusRef.current === VNCStatus.CONNECTING) return;
+      if (statusRef.current !== VNCStatus.DISCONNECTED) return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/vnc/useVNC.ts` around lines 205 - 212, Update the
early-return guard in the connect callback to also reject calls when
statusRef.current is VNCStatus.CONNECTED, preventing a second RFB session from
replacing the active rfbRef.current instance. Preserve the existing behavior for
CONNECTING and all other statuses.

134-134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

dotCursor is accepted but never applied.

connect hardcodes rfb.showDotCursor = false at Line 262 and ignores config.dotCursor. The VNC page forwards settings.dotCursor from the query parameters, so the user setting silently has no effect. Remove the field from VNCConfig or honor it in connect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/vnc/useVNC.ts` at line 134, Update the VNC
connection flow in connect to honor the optional VNCConfig.dotCursor value when
assigning rfb.showDotCursor, while preserving the existing default behavior when
the option is omitted; alternatively remove dotCursor from VNCConfig and all
callers if it is not intended to be supported.

378-384: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid using private noVNC scaling logic.

@novnc/novnc@1.7.0 provides scaleViewport, but _updateScale is not part of the public API. Use only public RFB/Display settings, or keep the custom resize handling in a dedicated module if the built-in behavior is insufficient.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/vnc/useVNC.ts` around lines 378 - 384, Update
handleResize in useVNC to stop calling the private rfbRef.current._updateScale
method. Use only the public scaleViewport API and supported RFB/Display
settings, or move any necessary custom resize behavior into a dedicated module
while preserving the existing debounced resize handling.
src/app/(remote-access)/peer/vnc/page.tsx (1)

134-145: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate error-notification helper.

The same helper exists in src/app/(remote-access)/peer/rdp/page.tsx (Lines 65-73). Extract one shared sendErrorNotification into a remote-access utility module and import it in both pages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(remote-access)/peer/vnc/page.tsx around lines 134 - 145, Extract
the duplicated sendErrorNotification helper from the VNC and RDP page components
into a shared remote-access utility module, preserving its notification options
and callback behavior. Import and use the shared helper in both pages, removing
their local definitions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/modules/remote-access/vnc/useVNC.ts`:
- Around line 502-515: Reset the remote-cursor state in the VNC disconnect
handler alongside setViewOnly(false). Add setShowRemoteCursorState(false) so a
new connection starts with the toolbar and server both showing the remote cursor
as disabled.
- Around line 489-500: The keydown and native paste handlers can send the same
clipboard text twice. In src/modules/remote-access/vnc/useVNC.ts lines 489-500,
update handlePasteShortcut to set a short-lived guard when it performs the eager
paste (and exclude Shift if Ctrl+Shift+V must reach the remote); in
src/app/(remote-access)/peer/vnc/page.tsx lines 488-498, check that guard and
skip vnc.pasteFromClipboardEvent when the keydown path already handled the
gesture.
- Around line 436-443: Update the paste flow around sendRawToVNCProxy and
clipboardPasteFrom so its return value reflects whether keystrokes were actually
sent: preserve the successful true result when the raw proxy send succeeds, but
return the appropriate failure or differentiated outcome after the
clipboard-only fallback. Ensure callers such as VNCToolbar can distinguish a
clipboard update from a successful typed paste.
- Around line 338-359: Update sendClipboard in the VNC setup to stop reading and
transmitting navigator.clipboard contents on every window focus. Trigger
clipboard synchronization only through an explicit user paste action or an
existing session setting, and replace the empty rejection handler with the
established error handling so rejected reads remain diagnosable.

---

Nitpick comments:
In `@src/app/`(remote-access)/peer/vnc/page.tsx:
- Around line 134-145: Extract the duplicated sendErrorNotification helper from
the VNC and RDP page components into a shared remote-access utility module,
preserving its notification options and callback behavior. Import and use the
shared helper in both pages, removing their local definitions.

In `@src/modules/remote-access/vnc/useVNC.ts`:
- Around line 205-212: Update the early-return guard in the connect callback to
also reject calls when statusRef.current is VNCStatus.CONNECTED, preventing a
second RFB session from replacing the active rfbRef.current instance. Preserve
the existing behavior for CONNECTING and all other statuses.
- Line 134: Update the VNC connection flow in connect to honor the optional
VNCConfig.dotCursor value when assigning rfb.showDotCursor, while preserving the
existing default behavior when the option is omitted; alternatively remove
dotCursor from VNCConfig and all callers if it is not intended to be supported.
- Around line 378-384: Update handleResize in useVNC to stop calling the private
rfbRef.current._updateScale method. Use only the public scaleViewport API and
supported RFB/Display settings, or move any necessary custom resize behavior
into a dedicated module while preserving the existing debounced resize handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 71269112-faec-4eaf-8f7c-f510d03892e6

📥 Commits

Reviewing files that changed from the base of the PR and between ba7f600 and 5f9a654.

📒 Files selected for processing (2)
  • src/app/(remote-access)/peer/vnc/page.tsx
  • src/modules/remote-access/vnc/useVNC.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 4

🧹 Nitpick comments (4)
src/modules/remote-access/vnc/useVNC.ts (3)

205-212: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Guard connect against an active session.

The early return only covers VNCStatus.CONNECTING. If a caller invokes connect while the status is CONNECTED, the hook creates a second RFB instance and overwrites rfbRef.current, so the previous session and its WebSocket leak. The page currently guards on VNCStatus.DISCONNECTED, so this is defensive only.

♻️ Proposed guard
-      if (statusRef.current === VNCStatus.CONNECTING) return;
+      if (statusRef.current !== VNCStatus.DISCONNECTED) return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/vnc/useVNC.ts` around lines 205 - 212, Update the
early-return guard in the connect callback to also reject calls when
statusRef.current is VNCStatus.CONNECTED, preventing a second RFB session from
replacing the active rfbRef.current instance. Preserve the existing behavior for
CONNECTING and all other statuses.

134-134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

dotCursor is accepted but never applied.

connect hardcodes rfb.showDotCursor = false at Line 262 and ignores config.dotCursor. The VNC page forwards settings.dotCursor from the query parameters, so the user setting silently has no effect. Remove the field from VNCConfig or honor it in connect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/vnc/useVNC.ts` at line 134, Update the VNC
connection flow in connect to honor the optional VNCConfig.dotCursor value when
assigning rfb.showDotCursor, while preserving the existing default behavior when
the option is omitted; alternatively remove dotCursor from VNCConfig and all
callers if it is not intended to be supported.

378-384: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid using private noVNC scaling logic.

@novnc/novnc@1.7.0 provides scaleViewport, but _updateScale is not part of the public API. Use only public RFB/Display settings, or keep the custom resize handling in a dedicated module if the built-in behavior is insufficient.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/vnc/useVNC.ts` around lines 378 - 384, Update
handleResize in useVNC to stop calling the private rfbRef.current._updateScale
method. Use only the public scaleViewport API and supported RFB/Display
settings, or move any necessary custom resize behavior into a dedicated module
while preserving the existing debounced resize handling.
src/app/(remote-access)/peer/vnc/page.tsx (1)

134-145: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate error-notification helper.

The same helper exists in src/app/(remote-access)/peer/rdp/page.tsx (Lines 65-73). Extract one shared sendErrorNotification into a remote-access utility module and import it in both pages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(remote-access)/peer/vnc/page.tsx around lines 134 - 145, Extract
the duplicated sendErrorNotification helper from the VNC and RDP page components
into a shared remote-access utility module, preserving its notification options
and callback behavior. Import and use the shared helper in both pages, removing
their local definitions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/modules/remote-access/vnc/useVNC.ts`:
- Around line 502-515: Reset the remote-cursor state in the VNC disconnect
handler alongside setViewOnly(false). Add setShowRemoteCursorState(false) so a
new connection starts with the toolbar and server both showing the remote cursor
as disabled.
- Around line 489-500: The keydown and native paste handlers can send the same
clipboard text twice. In src/modules/remote-access/vnc/useVNC.ts lines 489-500,
update handlePasteShortcut to set a short-lived guard when it performs the eager
paste (and exclude Shift if Ctrl+Shift+V must reach the remote); in
src/app/(remote-access)/peer/vnc/page.tsx lines 488-498, check that guard and
skip vnc.pasteFromClipboardEvent when the keydown path already handled the
gesture.
- Around line 436-443: Update the paste flow around sendRawToVNCProxy and
clipboardPasteFrom so its return value reflects whether keystrokes were actually
sent: preserve the successful true result when the raw proxy send succeeds, but
return the appropriate failure or differentiated outcome after the
clipboard-only fallback. Ensure callers such as VNCToolbar can distinguish a
clipboard update from a successful typed paste.
- Around line 338-359: Update sendClipboard in the VNC setup to stop reading and
transmitting navigator.clipboard contents on every window focus. Trigger
clipboard synchronization only through an explicit user paste action or an
existing session setting, and replace the empty rejection handler with the
established error handling so rejected reads remain diagnosable.

---

Nitpick comments:
In `@src/app/`(remote-access)/peer/vnc/page.tsx:
- Around line 134-145: Extract the duplicated sendErrorNotification helper from
the VNC and RDP page components into a shared remote-access utility module,
preserving its notification options and callback behavior. Import and use the
shared helper in both pages, removing their local definitions.

In `@src/modules/remote-access/vnc/useVNC.ts`:
- Around line 205-212: Update the early-return guard in the connect callback to
also reject calls when statusRef.current is VNCStatus.CONNECTED, preventing a
second RFB session from replacing the active rfbRef.current instance. Preserve
the existing behavior for CONNECTING and all other statuses.
- Line 134: Update the VNC connection flow in connect to honor the optional
VNCConfig.dotCursor value when assigning rfb.showDotCursor, while preserving the
existing default behavior when the option is omitted; alternatively remove
dotCursor from VNCConfig and all callers if it is not intended to be supported.
- Around line 378-384: Update handleResize in useVNC to stop calling the private
rfbRef.current._updateScale method. Use only the public scaleViewport API and
supported RFB/Display settings, or move any necessary custom resize behavior
into a dedicated module while preserving the existing debounced resize handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 71269112-faec-4eaf-8f7c-f510d03892e6

📥 Commits

Reviewing files that changed from the base of the PR and between ba7f600 and 5f9a654.

📒 Files selected for processing (2)
  • src/app/(remote-access)/peer/vnc/page.tsx
  • src/modules/remote-access/vnc/useVNC.ts
🛑 Comments failed to post (4)
src/modules/remote-access/vnc/useVNC.ts (4)

338-359: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Firefox navigator.clipboard.readText user activation paste prompt behavior

💡 Result:

In Firefox, the navigator.clipboard.readText method requires transient user activation to function, ensuring that clipboard access is initiated by the user [1][2][3]. When a script calls navigator.clipboard.readText, Firefox enforces a security model to protect user privacy: 1. Requirement for User Activation: The call must occur within the context of a transient user gesture (such as a click or keypress) [4][2][3]. If the call is made without this recent user interaction, the promise will be rejected with a DOMException (e.g., "Clipboard read request was blocked due to lack of user activation") [4][3]. 2. Paste Prompt Behavior: If the data on the clipboard did not originate from the same-origin page, Firefox will display a "Paste" context menu or prompt to the user [2][5]. The user must explicitly interact with this prompt to grant the page access to the clipboard contents [2][5]. If the user dismisses this prompt, the promise will be rejected [4]. 3. Same-Origin Exception: If the clipboard content originated from the same-origin page, access may be granted without an additional prompt, provided the transient user activation requirement is still met [2][5]. 4. Browser Context: This behavior is enabled by default in modern Firefox versions (starting from version 125/127 for different parts of the API) [6][7][2]. Browser extensions with appropriate permissions (e.g., clipboardRead) may be exempt from these specific user-activation and prompt requirements depending on their configuration [8][5]. All interactions with the Clipboard API must also occur within a secure context (HTTPS) [9][10].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant file and surrounding code without executing repository code.
if [ -f src/modules/remote-access/vnc/useVNC.ts ]; then
  echo "== file exists =="
  wc -l src/modules/remote-access/vnc/useVNC.ts
  echo
  echo "== relevant clipboard section 320-370 =="
  sed -n '320,370p' src/modules/remote-access/vnc/useVNC.ts | nl -ba -v320
  echo
  echo "== relevant Firefox/clipboard comment section 430-460 =="
  sed -n '430,460p' src/modules/remote-access/vnc/useVNC.ts | nl -ba -v430
else
  echo "file not found"
fi

echo
echo "== occurrences of sendClipboard and clipboardPasteFrom/readText in tracked files =="
rg -n "sendClipboard|clipboardPasteFrom|clipboard\.readText|Paste toolbar|window\.addEventListener\(\"focus\"" .

Repository: netbirdio/dashboard

Length of output: 302


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant clipboard section 320-370 =="
sed -n '320,370p' src/modules/remote-access/vnc/useVNC.ts | awk '{printf "%5d: %s\n", 319+NR, $0}'
echo
echo "== relevant Firefox/clipboard comment section 430-460 =="
sed -n '430,460p' src/modules/remote-access/vnc/useVNC.ts | awk '{printf "%5d: %s\n", 429+NR, $0}'
echo
echo "== occurrences of sendClipboard and clipboardPasteFrom/readText in tracked files =="
rg -n "sendClipboard|clipboardPasteFrom|readText|Paste toolbar|window\.addEventListener\(\"focus\"" .

Repository: netbirdio/dashboard

Length of output: 5416


Avoid reading the host clipboard without an explicit paste action.

sendClipboard() reads navigator.clipboard.readText() on every window focus and sends the result to the remote peer. A focus-based read can also be rejected by Firefox when the clipboard did not originate from the same-origin page. Keep this sync behind an explicit user action or session setting, and handle rejected clipboard reads without suppressing useful paste failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/vnc/useVNC.ts` around lines 338 - 359, Update
sendClipboard in the VNC setup to stop reading and transmitting
navigator.clipboard contents on every window focus. Trigger clipboard
synchronization only through an explicit user paste action or an existing
session setting, and replace the empty rejection handler with the established
error handling so rejected reads remain diagnosable.

436-443: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Return value does not reflect the fallback outcome.

When sendRawToVNCProxy returns false, the function only writes to the remote clipboard through clipboardPasteFrom. No keystrokes are typed. The function still returns true, so callers such as VNCToolbar report a successful paste. Return the actual result, or return a discriminated outcome so the UI can phrase the message correctly.

🐛 Proposed fix
     const sent = sendRawToVNCProxy(proxyIDRef.current, buf);
     if (!sent) {
       // Fall back to standard CutText so we at least update the OS clipboard
       // when the type path isn't available.
       rfbRef.current.clipboardPasteFrom(text);
     }
     rfbRef.current.focus();
-    return true;
+    return sent;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    const sent = sendRawToVNCProxy(proxyIDRef.current, buf);
    if (!sent) {
      // Fall back to standard CutText so we at least update the OS clipboard
      // when the type path isn't available.
      rfbRef.current.clipboardPasteFrom(text);
    }
    rfbRef.current.focus();
    return sent;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/vnc/useVNC.ts` around lines 436 - 443, Update the
paste flow around sendRawToVNCProxy and clipboardPasteFrom so its return value
reflects whether keystrokes were actually sent: preserve the successful true
result when the raw proxy send succeeds, but return the appropriate failure or
differentiated outcome after the clipboard-only fallback. Ensure callers such as
VNCToolbar can distinguish a clipboard update from a successful typed paste.

489-500: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Two paste paths can deliver the same text twice. The keydown interception and the native paste handler both type text on the remote. The shared root cause is that handlePasteShortcut pastes eagerly instead of leaving the paste gesture to the paste event, and neither path knows about the other.

  • src/modules/remote-access/vnc/useVNC.ts#L489-L500: add a short-lived guard set by handlePasteShortcut and checked by the paste-event path, or restrict this handler to browsers that withhold the paste event. Also add && !e.shiftKey if Ctrl+Shift+V must reach the remote.
  • src/app/(remote-access)/peer/vnc/page.tsx#L488-L498: skip vnc.pasteFromClipboardEvent when the guard indicates that the keydown path already pasted.
📍 Affects 2 files
  • src/modules/remote-access/vnc/useVNC.ts#L489-L500 (this comment)
  • src/app/(remote-access)/peer/vnc/page.tsx#L488-L498
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/vnc/useVNC.ts` around lines 489 - 500, The keydown
and native paste handlers can send the same clipboard text twice. In
src/modules/remote-access/vnc/useVNC.ts lines 489-500, update
handlePasteShortcut to set a short-lived guard when it performs the eager paste
(and exclude Shift if Ctrl+Shift+V must reach the remote); in
src/app/(remote-access)/peer/vnc/page.tsx lines 488-498, check that guard and
skip vnc.pasteFromClipboardEvent when the keydown path already handled the
gesture.

502-515: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset showRemoteCursor on disconnect.

The disconnect handler at Line 306 resets viewOnly but leaves showRemoteCursor. After a view-only session ends and the user reconnects, the toolbar still shows the remote cursor as enabled while the server starts with it disabled. Add setShowRemoteCursorState(false) next to setViewOnly(false).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/vnc/useVNC.ts` around lines 502 - 515, Reset the
remote-cursor state in the VNC disconnect handler alongside setViewOnly(false).
Add setShowRemoteCursorState(false) so a new connection starts with the toolbar
and server both showing the remote cursor as disabled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant