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

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

    • VNC remote access on peer pages with connect/reconnect flow, setup chooser, inline disconnected screen, and in-browser session UI including a draggable toolbar (Paste, optional Ctrl+Alt+Del, remote-cursor toggle, view-only badge) and contextual VNC access button/tooltips.
  • Bug Fixes / UX

    • Preserve VNC query params across auth/navigation and improve native cursor visibility for embedded VNC.
  • Chores

    • Added in-browser VNC integration, websocket proxy plumbing, and client/session connectivity support.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

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

Adds VNC remote access via noVNC over NetBird: types/query parsing, a WebSocket proxy bridge, a useVNC hook with lifecycle/clipboard handling, UI components (button/tooltip/toolbar), a /peer/vnc session page orchestrating NetBird and VNC, and dashboard/global wiring.

Changes

VNC Remote Access Integration

Layer / File(s) Summary
Types and query params
src/interfaces/Peer.ts, src/modules/remote-access/vnc/novnc.d.ts, src/modules/remote-access/vnc/useVNCQueryParams.ts
Adds server_vnc_allowed to PeerLocalFlags, ambient typings for @novnc/novnc (RFB/RFBOptions), and useVNCQueryParams to parse/restore VNC URL params and settings.
WebSocket proxy
src/modules/remote-access/vnc/websocket-proxy.ts
Implements VNCProxyWebSocket, per-proxy last-close metadata, sendRawToVNCProxy, and installVNCWebSocketProxy() to route vnc.proxy.local WebSocket traffic through Go/WASM handlers.
VNC connection hook
src/modules/remote-access/vnc/useVNC.ts
useVNC manages noVNC lifecycle: installs proxy, obtains proxy URL, dynamic-imports noVNC, creates RFB, enforces 20s timeout, registers connect/disconnect/security/clipboard handlers, and exposes clipboard/paste, Ctrl+Alt+Del, focus, and remote-cursor toggling.
Session page orchestration
src/app/(remote-access)/peer/vnc/page.tsx
New /peer/vnc page that fetches peer, shows setup chooser for session-capable OSes, orchestrates temporary NetBird tunnel and VNC auto-connect, handles reconnect/error flows, and renders toolbar + VNC canvas.
UI controls and toolbar
src/modules/remote-access/vnc/VNCButton.tsx, src/modules/remote-access/vnc/VNCTooltip.tsx, src/modules/remote-access/vnc/VNCToolbar.tsx
VNCButton (OS/permission gating, opens vnc page), VNCTooltip (contextual messages for permission/VNC enabled/offline), and VNCToolbar (draggable, persisted X% position, Paste and optional Ctrl+Alt+Del and remote-cursor toggle).
NetBird temporary connect changes
src/modules/remote-access/useNetBirdClient.ts
connectTemporary now returns TemporaryConnectResult, initializes WASM before temporary-access, optionally mints per-session X25519 key, includes session_pub_key in the request, and returns targetPubKey and keySessionId.
Dashboard & globals
package.json, src/app/(dashboard)/peer/page.tsx, src/modules/peers/PeerConnectButton.tsx, src/auth/SecureProvider.tsx, src/app/globals.css, src/components/ui/FullScreenLoading.tsx
Adds @novnc/novnc ^1.7.0, injects VNCButton into peer page and dropdown, preserves /peer/vnc query params in SecureProvider, updates FullScreenLoading to accept label, and forces cursor visibility via CSS overrides.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Dashboard
  participant VNCPage as /peer/vnc Page
  participant NetBird as useNetBirdClient
  participant VNC as useVNC Hook
  participant Proxy as VNCProxyWebSocket
  participant RFB as noVNC RFB
  participant Remote as Remote VNC Server

  User->>Dashboard: Click VNCButton
  Dashboard->>VNCPage: Open /peer/vnc?id=...
  VNCPage->>NetBird: ensure temporary tunnel for peer
  alt NetBird disconnected
    NetBird->>NetBird: create temporary tunnel (connectTemporary)
  end
  NetBird-->>VNCPage: tunnel ready (targetPubKey/keySessionId)
  VNCPage->>VNC: connect({ container, tunnelClient, token, mode })
  VNC->>NetBird: createVNCProxy() -> proxyURL/proxyID
  VNC->>RFB: dynamic import + new RFB(container, proxyURL)
  RFB->>Proxy: open WebSocket to vnc.proxy.local/<id>
  Proxy->>NetBird: forward data to remote via Go/WASM
  Remote-->>Proxy: VNC frames / clipboard / security
  Proxy-->>RFB: onmessage events
  RFB-->>VNC: "connect" event
  VNC-->>VNCPage: status=CONNECTED
  User->>VNCPage: Paste / Ctrl+Alt+Del / toolbar drag
  VNCPage->>VNC: pasteFromHostClipboard() or sendCtrlAltDel()
  VNC->>Proxy: sendRawToVNCProxy() or RFB.clipboardPasteFrom()
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

Suggested Reviewers

  • heisbrot
  • mlsmaycon

"I’m a rabbit with a tiny net,
tunneling desktops with nimble fret.
Clipboard hops and toolbar cheer,
VNC windows bright and near.
Hop on in — the screen is set!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 'Add embedded VNC viewer' directly reflects the main objective of the PR, which is to add a browser-based VNC viewer for peer connections alongside existing SSH/RDP integrations.
Description check ✅ Passed The PR description provides comprehensive details about the implementation (pages, noVNC integration, tunneling, session modes, controls, error handling) and addresses the template's documentation section with a clear explanation of why docs are not needed at this time.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 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 and usage tips.

@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
Comment on lines +21 to +26
const [xPercent, setXPercent] = useState<number>(() => {
if (typeof window === "undefined") return 50;
const stored = window.localStorage.getItem(STORAGE_KEY);
const parsed = stored ? parseFloat(stored) : NaN;
return isFinite(parsed) ? parsed : 50;
});

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard localStorage access in toolbar lifecycle paths.

Line 23 and Line 42 can throw in restricted/private storage contexts, which can crash or break toolbar interactions during a session.

🔧 Suggested fix
 export default function VNCToolbar({
@@
 }: Props) {
+  const clampPercent = (v: number) => Math.max(5, Math.min(95, v));
+
   const [xPercent, setXPercent] = useState<number>(() => {
     if (typeof window === "undefined") return 50;
-    const stored = window.localStorage.getItem(STORAGE_KEY);
-    const parsed = stored ? parseFloat(stored) : NaN;
-    return isFinite(parsed) ? parsed : 50;
+    try {
+      const stored = window.localStorage.getItem(STORAGE_KEY);
+      const parsed = stored ? parseFloat(stored) : NaN;
+      return isFinite(parsed) ? clampPercent(parsed) : 50;
+    } catch {
+      return 50;
+    }
   });
@@
     const onUp = () => {
       if (!draggingRef.current) return;
       draggingRef.current = false;
       document.body.style.userSelect = "";
-      window.localStorage.setItem(STORAGE_KEY, String(xPercentRef.current));
+      try {
+        window.localStorage.setItem(STORAGE_KEY, String(xPercentRef.current));
+      } catch {
+        // ignore storage failures; toolbar remains usable
+      }
     };

Also applies to: 38-43

🤖 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/VNCToolbar.tsx` around lines 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.

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