Skip to content

Add layer 4 protocol support to reverse proxy#579

Merged
heisbrot merged 52 commits intomainfrom
feature/reverse-proxy
Mar 18, 2026
Merged

Add layer 4 protocol support to reverse proxy#579
heisbrot merged 52 commits intomainfrom
feature/reverse-proxy

Conversation

@lixmal
Copy link
Copy Markdown
Contributor

@lixmal lixmal commented Mar 6, 2026

Summary

  • Add TCP, UDP, and TLS endpoint modes to the reverse proxy modal alongside the existing HTTP mode
  • Update proxy table cells (name, status, targets, auth) to display layer 4 protocol info
  • Support per-target options and flat target table for layer 4 proxies

Backend PR

netbirdio/netbird#5530

Summary by CodeRabbit

  • New Features

    • Service mode selector (HTTP/TLS/TCP/UDP) with mode-aware create/edit flows, Type column and mode badges, L4 target UI (port/listen port/address), target selector, bytes column in events, and human-friendly duration formatting.
    • New domain input/verification helpers and CIDR-aware address input; UDP session idle timeout support with validation.
  • Bug Fixes / UX

    • Auth shows N/A for L4 services; clearer hosted/offline messaging; Select items support icons/descriptions; RadioCard disabled state; DeviceCard hides empty descriptions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 6, 2026

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
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Adds service mode (HTTP/TLS/TCP/UDP) support across reverse-proxy: new types and helpers, provider exposure of resources/peers, mode-aware modal/target UI and payloads, L4-specific (TCP/UDP/TLS) flows and validations, table/cell rendering changes, and related UI components/hooks.

Changes

Cohort / File(s) Summary
Core types & provider
src/interfaces/ReverseProxy.ts, src/contexts/ReverseProxiesProvider.tsx
Add ServiceMode, EventProtocol, isL4 helpers; extend ReverseProxy, target options, domain shapes; provider now fetches/exposes resources and peers and adjusts API call init.
Modal & target flows
src/modules/reverse-proxy/ReverseProxyModal.tsx, src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx, src/modules/reverse-proxy/ReverseProxyServiceModeSelector.tsx
Introduce endpoint/mode state, initialEndpointMode prop, mode-aware UI branching (HTTP vs L4), new L4 inputs (listen_port/port, CIDR/host handling), and unified Target selector/editing flow.
L4 UI components & helpers
src/modules/reverse-proxy/ReverseProxyLayer4Content.tsx, src/modules/reverse-proxy/targets/ReverseProxyAddressInput.tsx, src/modules/reverse-proxy/targets/ReverseProxyTargetSelector.tsx, src/modules/reverse-proxy/targets/ReverseProxyTargetDevice.tsx
New components and hook for address/CIDR handling, target selector, L4 content layout, and device rendering with address/wrapper class props.
Targets validation & options
src/modules/reverse-proxy/targets/useReverseProxyTargetOptions.ts, src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx
Add session_idle_timeout validation and errors plumbing (UDP), integrate timeout/options into save logic.
Table, cells & events
src/modules/reverse-proxy/table/..., src/modules/reverse-proxy/events/...
Add Type column and ReverseProxyTypeCell/ServiceIcon, pass isL4 to status/auth/targets cells, adapt cell rendering for L4 (N/A badges, different status), add events bytes/duration formatting and service-aware URL column.
New UI pieces & domain helpers
src/modules/reverse-proxy/ReverseProxyHTTPTargets.tsx, src/modules/reverse-proxy/domain/*, src/modules/reverse-proxy/ReverseProxyServiceModeSelector.tsx
New HTTP targets list component, domain input and domain hook (useReverseProxyDomain), hosting-aware callouts, and service mode selector with SERVICE_MODES export.
Shared UI & minor updates
src/components/Select.tsx, src/components/RadioCard.tsx, src/assets/icons/ReverseProxyIcon.tsx, src/components/DeviceCard.tsx
SelectItem supports icon/description, RadioCard accepts disabled, SVG fill adjustments, conditional description render in DeviceCard.
Utilities
src/utils/helpers.ts
Add formatDuration(ms) helper.

Sequence Diagram

sequenceDiagram
    participant User
    participant Provider as ReverseProxiesProvider
    participant Modal as ReverseProxyModal
    participant TargetModal as ReverseProxyTargetModal
    participant Validation as useReverseProxyTargetOptions
    participant API as Backend/API

    User->>Provider: open reverse-proxy modal (optional initialEndpointMode)
    Provider->>Modal: pass resources, peers, initialEndpointMode
    Modal->>Modal: set endpointMode (reverseProxy.mode || initialEndpointMode)

    alt HTTP/TLS flow
        User->>Modal: configure domain/subdomain and HTTP targets
        Modal->>TargetModal: open HTTP/S target editor
        TargetModal->>Validation: validate headers/timeouts
    else L4 (TCP/UDP/TLS) flow
        User->>Modal: select L4 mode, configure listen_port and L4 target
        Modal->>TargetModal: open L4 target editor (ReverseProxyLayer4Content)
        TargetModal->>Validation: validate session_idle_timeout (UDP) and options
    end

    Validation-->>TargetModal: return errors or ok
    alt valid
        TargetModal->>API: submit payload (includes mode, listen_port, targets, options)
        API-->>Modal: persist response
    else invalid
        TargetModal-->>User: show validation errors
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • mlsmaycon
  • pascal-fischer

Poem

🐰
I hopped through props and chose the mode,
Four lanes now carry every load.
Ports, peers, timeouts — tidy and bright,
A rabbit’s tweak to set things right.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "Add layer 4 protocol support to reverse proxy" is a clear, concise summary that accurately reflects the main objective of the changeset—introducing TCP, UDP, and TLS (Layer 4) protocol support to the reverse proxy system.

✏️ 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 feature/reverse-proxy
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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/reverse-proxy/table/ReverseProxyTargetsCell.tsx (1)

62-98: Consider lifting peer/resource data to avoid redundant fetches.

The L4TargetDisplay component fetches peers and resources via useFetchApi. While SWR caches these requests, the parent table likely already has access to this data. Consider passing it as props if performance becomes a concern with many L4 services.

The current implementation is functional. This is a minor optimization that could be addressed later if needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/table/ReverseProxyTargetsCell.tsx` around lines 62
- 98, L4TargetDisplay currently calls useFetchApi for peers and resources inside
the component causing redundant fetches; change the component signature to
accept optional props (e.g., peers?: Peer[] and resources?: NetworkResource[])
and use those passed-in arrays to resolve peer/resource (falling back to the
existing useFetchApi calls only if the props are undefined), update all callers
of L4TargetDisplay to pass the parent’s cached peers/resources where available,
and keep the existing DeviceCard/address resolution logic unchanged (references:
L4TargetDisplay, useFetchApi, DeviceCard).
src/contexts/ReverseProxiesProvider.tsx (1)

57-57: Consider using ServiceMode enum for type consistency.

The initialEndpointMode uses string literals ("http" | "tls" | "tcp" | "udp") that mirror ServiceMode enum values. Using the enum directly would ensure type safety and prevent drift if enum values change.

♻️ Suggested refactor
+import { ServiceMode } from "@/interfaces/ReverseProxy";

 type OpenModalOptions = {
   proxy?: ReverseProxy;
   initialTab?: string;
-  initialEndpointMode?: "http" | "tls" | "tcp" | "udp";
+  initialEndpointMode?: ServiceMode;
   initialPeer?: Peer;
   ...
 };

-const [modalInitialEndpointMode, setModalInitialEndpointMode] = useState<
-  "http" | "tls" | "tcp" | "udp" | undefined
->();
+const [modalInitialEndpointMode, setModalInitialEndpointMode] = useState<
+  ServiceMode | undefined
+>();

Also applies to: 150-152

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/contexts/ReverseProxiesProvider.tsx` at line 57, The prop/type uses a
string union for endpoint modes; switch the type of initialEndpointMode to the
ServiceMode enum to ensure consistency: update the prop/interface in
ReverseProxiesProvider (initialEndpointMode) to use ServiceMode, import
ServiceMode where needed, and update any state, default values, and comparisons
(the other occurrences around the initial state/usage) to use ServiceMode enum
members instead of raw strings so all references (initialEndpointMode, any state
setters or comparisons) remain type-safe and consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/reverse-proxy/ReverseProxyModal.tsx`:
- Around line 294-296: The modal currently always reads/writes the timeout into
request_timeout (state variables requestTimeout/setRequestTimeout and
existingL4Target?.options?.request_timeout), which breaks the UDP path that
expects session_idle_timeout; change initialization to read from
existingL4Target?.options?.session_idle_timeout when the selected L4 mode is UDP
and change the serialization logic that builds the target options to write
session_idle_timeout instead of request_timeout for UDP services (leave
request_timeout for other L4 modes). Ensure the timeout collectors (the places
that gather values for all L4 modes) and the save/submit code use the selected
mode to choose which key (request_timeout vs session_idle_timeout) to persist so
values round-trip correctly.
- Around line 214-245: Currently the code only hydrates existingL4Target from
reverseProxy.targets?.[0] and always submits a single-element array [l4Target],
which discards any other L4 targets; change the logic to treat L4 targets as an
array: hydrate state from reverseProxy.targets (preserve the whole array)
instead of only targets[0] (replace existingL4Target references with the targets
array or a selectedIndex), keep per-target fields (tlsTargetType, tlsPeerId,
tlsResourceId, tlsHost, tlsPort, tlsListenPort) tied to the selected target
index or maintain a targets state like l4Targets: ReverseProxyTarget[], update
the correct target in that array when fields change (or replace the target at
index 0 only if the user intends) and, on submit, send reverseProxy.targets (the
full updated array) instead of always submitting [l4Target]; update places
referencing existingL4Target and the submit code that builds l4Target to
read/write from this array (e.g., existingL4Target, tls* state variables, and
the code that constructs l4Target for submission).
- Around line 218-230: The initial state logic for TLS target selection in
ReverseProxyModal sets tlsTargetType to PEER regardless of initialResource,
causing resource-driven create flows to start with no selected target; update
the initializer for tlsTargetType to choose ReverseProxyTargetType.RESOURCE when
initialResource is present (instead of defaulting to PEER), and ensure
tlsResourceId is initialized from initialResource?.id while tlsPeerId is
undefined when initialResource exists; adjust the related setters
(setTlsTargetType, setTlsPeerId, setTlsResourceId) so existingL4Target and
existingL4IsPeer remain honored but initialResource takes precedence when no
existing target is provided.

---

Nitpick comments:
In `@src/contexts/ReverseProxiesProvider.tsx`:
- Line 57: The prop/type uses a string union for endpoint modes; switch the type
of initialEndpointMode to the ServiceMode enum to ensure consistency: update the
prop/interface in ReverseProxiesProvider (initialEndpointMode) to use
ServiceMode, import ServiceMode where needed, and update any state, default
values, and comparisons (the other occurrences around the initial state/usage)
to use ServiceMode enum members instead of raw strings so all references
(initialEndpointMode, any state setters or comparisons) remain type-safe and
consistent.

In `@src/modules/reverse-proxy/table/ReverseProxyTargetsCell.tsx`:
- Around line 62-98: L4TargetDisplay currently calls useFetchApi for peers and
resources inside the component causing redundant fetches; change the component
signature to accept optional props (e.g., peers?: Peer[] and resources?:
NetworkResource[]) and use those passed-in arrays to resolve peer/resource
(falling back to the existing useFetchApi calls only if the props are
undefined), update all callers of L4TargetDisplay to pass the parent’s cached
peers/resources where available, and keep the existing DeviceCard/address
resolution logic unchanged (references: L4TargetDisplay, useFetchApi,
DeviceCard).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eeb2821f-f31c-4976-8ddc-d61751b6001f

📥 Commits

Reviewing files that changed from the base of the PR and between 60e8394 and 4e93fff.

📒 Files selected for processing (11)
  • src/contexts/ReverseProxiesProvider.tsx
  • src/interfaces/ReverseProxy.ts
  • src/modules/reverse-proxy/ReverseProxyModal.tsx
  • src/modules/reverse-proxy/table/ReverseProxyAuthCell.tsx
  • src/modules/reverse-proxy/table/ReverseProxyNameCell.tsx
  • src/modules/reverse-proxy/table/ReverseProxyStatusCell.tsx
  • src/modules/reverse-proxy/table/ReverseProxyTable.tsx
  • src/modules/reverse-proxy/table/ReverseProxyTargetsCell.tsx
  • src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx
  • src/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTable.tsx
  • src/modules/reverse-proxy/targets/useReverseProxyTargetOptions.ts

Comment thread src/modules/reverse-proxy/ReverseProxyModal.tsx Outdated
Comment thread src/modules/reverse-proxy/ReverseProxyModal.tsx Outdated
Comment thread src/modules/reverse-proxy/ReverseProxyModal.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/modules/reverse-proxy/ReverseProxyModal.tsx (1)

218-229: ⚠️ Potential issue | 🟠 Major

Restore initialResource in the L4 initial state.

When there is no existingL4Target, existingL4IsPeer is false, so Line 228 still takes the existingL4Target?.target_id branch and drops initialResource?.id. Line 222 also hard-codes every preset resource to HOST, which loses subnet targets. Resource-driven create flows will still open with no selected target, and subnet resources can be serialized with the wrong type.

Proposed fix
   const [tlsTargetType, setTlsTargetType] = useState<ReverseProxyTargetType>(
     existingL4Target
       ? existingL4Target.target_type
       : initialResource
-        ? ReverseProxyTargetType.HOST
+        ? (initialResource.type as ReverseProxyTargetType)
         : ReverseProxyTargetType.PEER,
   );
   const [tlsPeerId, setTlsPeerId] = useState<string | undefined>(
     existingL4IsPeer ? existingL4Target?.target_id : initialPeer?.id,
   );
   const [tlsResourceId, setTlsResourceId] = useState<string | undefined>(
-    !existingL4IsPeer ? existingL4Target?.target_id : initialResource?.id,
+    existingL4Target
+      ? existingL4IsPeer
+        ? undefined
+        : existingL4Target.target_id
+      : initialResource?.id,
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/ReverseProxyModal.tsx` around lines 218 - 229, The
initial L4 state ignores initialResource when no existingL4Target, causing HOST
to be hard-coded and dropping subnet/other resource types; update the
initializers for tlsTargetType, tlsPeerId and tlsResourceId so they first check
existingL4Target (and existingL4IsPeer for target id), and if absent fall back
to initialResource (using initialResource?.type for tlsTargetType and
initialResource?.id for tlsResourceId) and only then to the default (e.g.,
ReverseProxyTargetType.PEER) or initialPeer for tlsPeerId; adjust the logic
around tlsTargetType, tlsPeerId and tlsResourceId to use existingL4Target ? ...
: initialResource ? ... : ... so resource-driven create flows preserve
subnet/resource types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/reverse-proxy/ReverseProxyModal.tsx`:
- Around line 564-577: The port-based branch (isPortBased) removed the UI bound
to subdomain so TCP/UDP services can't be named or renamed; restore a name field
in that branch by rendering an input (matching the non-port branch pattern)
bound to the subdomain state (value={subdomain}, onChange={setSubdomain})—e.g.,
add a Label/HelpText + text input next to CustomDomainSelector inside the
isPortBased block so the code that uses subdomain (service naming logic
referencing subdomain) continues to work for new and existing TCP/UDP services.
- Around line 1008-1018: The Connection Timeout UI block rendered when isL4Mode
is true currently uses TCP/TLS wording; update the Label and HelpText in the JSX
block (the element containing Timer, "Connection Timeout" Label and the
HelpText) to be mode-aware: when the L4 backend protocol is UDP show a label
like "Session Idle Timeout" and help text that explains this saves
session_idle_timeout (idle timeout for UDP sessions), otherwise keep the
existing "Connection Timeout" and text about establishing backend connections
for TCP/TLS; implement the conditional using the existing isL4Mode/protocol
state (or the same prop that distinguishes UDP) inside the same component so
copy is accurate for both UDP and non-UDP L4 modes.

---

Duplicate comments:
In `@src/modules/reverse-proxy/ReverseProxyModal.tsx`:
- Around line 218-229: The initial L4 state ignores initialResource when no
existingL4Target, causing HOST to be hard-coded and dropping subnet/other
resource types; update the initializers for tlsTargetType, tlsPeerId and
tlsResourceId so they first check existingL4Target (and existingL4IsPeer for
target id), and if absent fall back to initialResource (using
initialResource?.type for tlsTargetType and initialResource?.id for
tlsResourceId) and only then to the default (e.g., ReverseProxyTargetType.PEER)
or initialPeer for tlsPeerId; adjust the logic around tlsTargetType, tlsPeerId
and tlsResourceId to use existingL4Target ? ... : initialResource ? ... : ... so
resource-driven create flows preserve subnet/resource types.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dea610bf-ce3f-4175-b169-98521f576aed

📥 Commits

Reviewing files that changed from the base of the PR and between 4e93fff and bd5b805.

📒 Files selected for processing (1)
  • src/modules/reverse-proxy/ReverseProxyModal.tsx

Comment thread src/modules/reverse-proxy/ReverseProxyModal.tsx Outdated
Comment thread src/modules/reverse-proxy/ReverseProxyModal.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/modules/reverse-proxy/ReverseProxyModal.tsx (1)

218-233: ⚠️ Potential issue | 🟠 Major

Keep preselected resource targets typed as the actual resource.

initialResource is now forced to HOST, and the CIDR lookup only comes from the async /networks/resources result. If this modal is opened from a subnet resource, the payload can still go out as a host target and skip CIDR host validation before that lookup finishes.

🛠️ Suggested fix
  const [tlsTargetType, setTlsTargetType] = useState<ReverseProxyTargetType>(
    existingL4Target
      ? existingL4Target.target_type
      : initialResource
-        ? ReverseProxyTargetType.HOST
+        ? (initialResource.type as ReverseProxyTargetType)
         : ReverseProxyTargetType.PEER,
  );
  const [tlsPeerId, setTlsPeerId] = useState<string | undefined>(
-    existingL4IsPeer ? existingL4Target?.target_id : initialPeer?.id,
+    existingL4IsPeer
+      ? existingL4Target?.target_id
+      : initialResource
+        ? undefined
+        : initialPeer?.id,
  );
  const [tlsResourceId, setTlsResourceId] = useState<string | undefined>(
    existingL4Target
      ? existingL4IsPeer
        ? undefined
        : existingL4Target.target_id
      : initialResource?.id,
  );

  const tlsResourceAddress = useMemo(() => {
    if (!tlsResourceId) return "";
-    const resource = resources?.find((r) => r.id === tlsResourceId);
+    const resource =
+      resources?.find((r) => r.id === tlsResourceId) ??
+      (initialResource?.id === tlsResourceId ? initialResource : undefined);
     return resource?.address || "";
-  }, [tlsResourceId, resources]);
+  }, [tlsResourceId, resources, initialResource]);

Also applies to: 252-281

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/ReverseProxyModal.tsx` around lines 218 - 233, The
modal currently forces preselected initialResource to
ReverseProxyTargetType.HOST which can send a host payload and skip CIDR
validation when initialResource is actually a subnet; update the initial state
logic used in tlsTargetType (and the similar blocks around
tlsPeerId/tlsResourceId between the other mentioned lines) to infer the correct
ReverseProxyTargetType from initialResource.type (e.g., NETWORK/CIDR vs HOST)
instead of always defaulting to HOST, and ensure the initial tlsResourceId is
set accordingly so CIDR validation is applied until the async
/networks/resources lookup confirms the resource details.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/reverse-proxy/ReverseProxyModal.tsx`:
- Around line 524-536: The modal title logic uses initialEndpointMode instead of
the live selection, so when creating a new service the header doesn't update; in
ReverseProxyModal.tsx update the title branch that runs when reverseProxy is
falsy to consult endpointMode (the state/prop holding the current selection)
rather than initialEndpointMode so the title shows "Add TLS Passthrough"/"Add
TCP Service"/"Add UDP Service"/"Add HTTP Service" based on the current
endpointMode; keep the existing reverseProxy check and only swap
initialEndpointMode → endpointMode in the ternary chain used to set title.
- Around line 652-671: The button list currently only uses color to indicate
selection; update the component so assistive tech can detect the selected
endpoint by adding appropriate ARIA radio semantics: wrap the buttons' container
(where the map over modes occurs) with role="radiogroup" and for each button
(the element using setEndpointMode, endpointMode and mode) set role="radio" and
aria-checked={endpointMode === mode} (or aria-pressed if you prefer toggle
semantics) and ensure the onClick still calls setEndpointMode(mode); also add
tabIndex={endpointMode === mode ? 0 : -1} so keyboard focus moves to the
selected item and include an accessible label via aria-label or use the existing
label text.

---

Duplicate comments:
In `@src/modules/reverse-proxy/ReverseProxyModal.tsx`:
- Around line 218-233: The modal currently forces preselected initialResource to
ReverseProxyTargetType.HOST which can send a host payload and skip CIDR
validation when initialResource is actually a subnet; update the initial state
logic used in tlsTargetType (and the similar blocks around
tlsPeerId/tlsResourceId between the other mentioned lines) to infer the correct
ReverseProxyTargetType from initialResource.type (e.g., NETWORK/CIDR vs HOST)
instead of always defaulting to HOST, and ensure the initial tlsResourceId is
set accordingly so CIDR validation is applied until the async
/networks/resources lookup confirms the resource details.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 66dcd4e7-f590-4479-b247-4a524be842cb

📥 Commits

Reviewing files that changed from the base of the PR and between bd5b805 and cb4a919.

📒 Files selected for processing (1)
  • src/modules/reverse-proxy/ReverseProxyModal.tsx

Comment thread src/modules/reverse-proxy/ReverseProxyModal.tsx Outdated
Comment thread src/modules/reverse-proxy/ReverseProxyModal.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTable.tsx (1)

206-253: Consider extracting the Add Service dropdown into a shared component.

This dropdown implementation is nearly identical to AddServiceDropdown in ReverseProxyTable.tsx (lines 273-322). Extracting it into a shared component would reduce duplication and ensure consistent behavior across tables.

♻️ Example extraction

Create a shared component:

// src/modules/reverse-proxy/components/AddServiceDropdown.tsx
type AddServiceDropdownProps = {
  openModal: (options?: { initialEndpointMode?: ServiceMode }) => void;
  disabled?: boolean;
  className?: string;
};

export function AddServiceDropdown({ openModal, disabled, className }: Readonly<AddServiceDropdownProps>) {
  return (
    <DropdownMenu modal={false}>
      <DropdownMenuTrigger asChild>
        <Button variant="primary" disabled={disabled} className={className}>
          <PlusCircle size={16} />
          Add Service
          <ChevronDown size={14} />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end" className="w-auto min-w-[200px]">
        {/* ... menu items ... */}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTable.tsx`
around lines 206 - 253, The Add Service dropdown in ReverseProxyFlatTargetsTable
is duplicated from the one in ReverseProxyTable.tsx; extract it into a shared
component (e.g., AddServiceDropdown) that accepts props { openModal: (opts?: {
initialEndpointMode?: ServiceMode }) => void, disabled?: boolean, className?:
string }, move the DropdownMenu, DropdownMenuTrigger/Button and
DropdownMenuContent/MenuItems into that component, replace the inline JSX in
ReverseProxyFlatTargetsTable and ReverseProxyTable to render <AddServiceDropdown
openModal={openModal} disabled={!permission?.services?.create}
className="ml-auto" />, and ensure the menu item handlers still call openModal({
initialEndpointMode: ServiceMode.HTTP|TLS|TCP|UDP }) so behavior and icons are
preserved.
src/modules/reverse-proxy/ReverseProxyModal.tsx (1)

777-781: IPv4-only character filter may limit IPv6 support.

The regex .replace(/[^0-9.]/g, "") restricts input to IPv4-compatible characters only. If subnet resources with IPv6 addresses are supported, this filter would need to allow colons and hex characters (e.g., /[^0-9a-fA-F:.]/g).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/ReverseProxyModal.tsx` around lines 777 - 781, The
onChange handler inside ReverseProxyModal that setsTlsHost currently strips all
characters except digits and dots using e.target.value.replace(/[^0-9.]/g,""),
which prevents IPv6 addresses; update the filter used in the onChange for
setTlsHost (and any related input sanitization for tlsIsHostEditable) to allow
IPv6 characters (hex digits, colons, and dots) or replace the ad-hoc regex with
a proper validation/normalization function (e.g., use a regex permitting 0-9 a-f
A-F : . or a dedicated IPv4/IPv6 parser) so IPv6 host inputs are accepted while
still rejecting invalid characters.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/reverse-proxy/ReverseProxyModal.tsx`:
- Line 491: The assignment uses an unsafe double-cast endpointMode as unknown as
ReverseProxyTargetProtocol because ReverseProxyTargetProtocol only allows "http"
| "https" while ServiceMode includes "tcp" | "udp" | "tls"; fix by expanding the
type system instead of casting: update the ReverseProxyTargetProtocol type (or
enum) to include "tcp", "udp", "tls", or introduce a new union (e.g.,
ReverseProxyTargetProtocolL4 or Protocol = ReverseProxyTargetProtocol |
ServiceMode) and update the ReverseProxyTarget.protocol type accordingly, then
remove the cast in ReverseProxyModal.tsx and assign endpointMode directly to
protocol (or narrow/convert types explicitly). Ensure references to
ReverseProxyTarget, ReverseProxyTargetProtocol, and endpointMode are updated
consistently.

---

Nitpick comments:
In `@src/modules/reverse-proxy/ReverseProxyModal.tsx`:
- Around line 777-781: The onChange handler inside ReverseProxyModal that
setsTlsHost currently strips all characters except digits and dots using
e.target.value.replace(/[^0-9.]/g,""), which prevents IPv6 addresses; update the
filter used in the onChange for setTlsHost (and any related input sanitization
for tlsIsHostEditable) to allow IPv6 characters (hex digits, colons, and dots)
or replace the ad-hoc regex with a proper validation/normalization function
(e.g., use a regex permitting 0-9 a-f A-F : . or a dedicated IPv4/IPv6 parser)
so IPv6 host inputs are accepted while still rejecting invalid characters.

In `@src/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTable.tsx`:
- Around line 206-253: The Add Service dropdown in ReverseProxyFlatTargetsTable
is duplicated from the one in ReverseProxyTable.tsx; extract it into a shared
component (e.g., AddServiceDropdown) that accepts props { openModal: (opts?: {
initialEndpointMode?: ServiceMode }) => void, disabled?: boolean, className?:
string }, move the DropdownMenu, DropdownMenuTrigger/Button and
DropdownMenuContent/MenuItems into that component, replace the inline JSX in
ReverseProxyFlatTargetsTable and ReverseProxyTable to render <AddServiceDropdown
openModal={openModal} disabled={!permission?.services?.create}
className="ml-auto" />, and ensure the menu item handlers still call openModal({
initialEndpointMode: ServiceMode.HTTP|TLS|TCP|UDP }) so behavior and icons are
preserved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7882199c-cdd4-4dea-a3d6-38c860d4d419

📥 Commits

Reviewing files that changed from the base of the PR and between cb4a919 and 3768959.

📒 Files selected for processing (4)
  • src/contexts/ReverseProxiesProvider.tsx
  • src/modules/reverse-proxy/ReverseProxyModal.tsx
  • src/modules/reverse-proxy/table/ReverseProxyTable.tsx
  • src/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTable.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/contexts/ReverseProxiesProvider.tsx

Comment thread src/modules/reverse-proxy/ReverseProxyModal.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/modules/reverse-proxy/ReverseProxyModal.tsx (2)

208-212: Defer L4 target data loading until an L4 flow actually needs it.

/peers and /networks/resources are fetched even for HTTP-only opens, where this data is never used. Gating or lazy-loading these requests would avoid extra API traffic on the common path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/ReverseProxyModal.tsx` around lines 208 - 212, The
component ReverseProxyModal is eagerly fetching peers and network resources via
useFetchApi("/peers") and useFetchApi("/networks/resources"); change this to
lazy/gated loading so those requests only run when an L4 target is required
(e.g., when the selected protocol/flow indicates TCP/UDP). Implement this by
moving or conditionally calling the fetch logic (or passing an enabled flag)
tied to the L4 selection state used by the TLS target selector, so peers and
resources are requested only after the modal state indicates an L4 flow; update
any handlers that open the TLS/L4 target UI to trigger the fetch and surface
loading state accordingly.

487-497: Drop the remaining protocol double-cast.

endpointMode as string as ReverseProxyTargetProtocol still sidesteps the type checker, so ServiceMode and ReverseProxyTargetProtocol can drift apart again without a compile-time failure. Prefer an explicit mapper or a shared union here.

♻️ Safer direction
+const toL4Protocol = (mode: ServiceMode): ReverseProxyTargetProtocol => {
+  switch (mode) {
+    case ServiceMode.TCP:
+      return ReverseProxyTargetProtocol.TCP;
+    case ServiceMode.UDP:
+      return ReverseProxyTargetProtocol.UDP;
+    case ServiceMode.TLS:
+      return ReverseProxyTargetProtocol.TLS;
+    default:
+      throw new Error(`Unsupported L4 mode: ${mode}`);
+  }
+};
+
     const l4Target: ReverseProxyTarget = {
       target_id: tlsPeerId || tlsResourceId || "",
       target_type: l4TargetType,
       port: tlsPort,
-      protocol: endpointMode as string as ReverseProxyTargetProtocol,
+      protocol: toL4Protocol(endpointMode),
       host: tlsIsCidrRange ? tlsHost : undefined,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/ReverseProxyModal.tsx` around lines 487 - 497, The
code constructs l4Target with protocol: endpointMode as string as
ReverseProxyTargetProtocol which bypasses type safety; replace that double-cast
by mapping endpointMode to a ReverseProxyTargetProtocol explicitly (either via a
small mapper function or a switch/lookup) and use the mapper's return type so
protocol is strongly typed; update any usages of endpointMode in
ReverseProxyModal (the l4Target object) to call the mapper (or use a shared
union type) instead of the double-cast to ensure compile-time alignment between
ServiceMode/endpointMode and ReverseProxyTargetProtocol.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/reverse-proxy/ReverseProxyModal.tsx`:
- Around line 756-808: Add accessible labels for each of the three L4 inputs
(public port, host/IP, destination port) so screen readers can identify them;
locate the three Input components that use setTlsListenPort, setTlsHost, and
setTlsPort and give each a unique id and either a visible <label htmlFor="...">
or an aria-label/aria-labelledby that describes the control (e.g., "Public
port", "Host or IP", "Destination port"). Ensure the host input's
readOnly/disabled states still have an accessible label and update any
placeholder usage so it doesn't serve as the only accessible name.

---

Nitpick comments:
In `@src/modules/reverse-proxy/ReverseProxyModal.tsx`:
- Around line 208-212: The component ReverseProxyModal is eagerly fetching peers
and network resources via useFetchApi("/peers") and
useFetchApi("/networks/resources"); change this to lazy/gated loading so those
requests only run when an L4 target is required (e.g., when the selected
protocol/flow indicates TCP/UDP). Implement this by moving or conditionally
calling the fetch logic (or passing an enabled flag) tied to the L4 selection
state used by the TLS target selector, so peers and resources are requested only
after the modal state indicates an L4 flow; update any handlers that open the
TLS/L4 target UI to trigger the fetch and surface loading state accordingly.
- Around line 487-497: The code constructs l4Target with protocol: endpointMode
as string as ReverseProxyTargetProtocol which bypasses type safety; replace that
double-cast by mapping endpointMode to a ReverseProxyTargetProtocol explicitly
(either via a small mapper function or a switch/lookup) and use the mapper's
return type so protocol is strongly typed; update any usages of endpointMode in
ReverseProxyModal (the l4Target object) to call the mapper (or use a shared
union type) instead of the double-cast to ensure compile-time alignment between
ServiceMode/endpointMode and ReverseProxyTargetProtocol.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4b814aa9-b673-45a5-933d-17ec978fbe72

📥 Commits

Reviewing files that changed from the base of the PR and between 3768959 and 65d1949.

📒 Files selected for processing (2)
  • src/interfaces/ReverseProxy.ts
  • src/modules/reverse-proxy/ReverseProxyModal.tsx

Comment thread src/modules/reverse-proxy/ReverseProxyModal.tsx Outdated
@lixmal lixmal force-pushed the feature/reverse-proxy branch from d4b33bf to fbe7ff6 Compare March 9, 2026 17:10
@lixmal lixmal force-pushed the feature/reverse-proxy branch from 22c9fd7 to 5033cae Compare March 11, 2026 07:12
@heisbrot
Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 18, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (1)
src/components/DeviceCard.tsx (1)

28-32: ⚠️ Potential issue | 🟡 Minor

Missing resource in useMemo dependency array.

The computation references resource?.address but resource is not listed in the dependency array. If resource updates while description, address, and device remain unchanged, descriptionText will be stale.

🐛 Proposed fix
   const descriptionText = useMemo(() => {
     return description !== undefined
       ? description
       : address || device?.ip || resource?.address;
-  }, [description, address, device]);
+  }, [description, address, device, resource]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/DeviceCard.tsx` around lines 28 - 32, The useMemo that
computes descriptionText references resource?.address but omits resource from
its dependency array; update the dependency list for the useMemo that returns
descriptionText to include resource (e.g., [description, address, device,
resource]) so descriptionText recalculates when resource changes, ensuring the
computed value stays current.
🧹 Nitpick comments (3)
src/utils/helpers.ts (1)

266-275: Consider validating edge cases for negative or NaN inputs.

The function handles positive durations well, but edge cases may produce unexpected results:

  • Negative values (e.g., -500) would return "-500ms"
  • NaN would return "NaNh" (all comparisons return false)
🛡️ Suggested defensive handling
 export const formatDuration = (ms: number): string => {
   try {
+    if (!Number.isFinite(ms) || ms < 0) return "0ms";
     if (ms < 1000) return `${ms}ms`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/helpers.ts` around lines 266 - 275, The formatDuration function
should defensively handle negative and NaN inputs: detect if ms is NaN or less
than 0 and return "0ms" (or clamp to 0 before formatting) instead of producing
"-500ms" or "NaNh"; update the logic in formatDuration to validate the input at
the top (using Number.isFinite or Number.isNaN checks and a negative check) and
then proceed with the existing branch formatting so all outputs remain sensible
for invalid inputs.
src/assets/icons/ReverseProxyIcon.tsx (1)

11-16: Minor: Redundant fill attribute on SVG element.

The fill="currentColor" on line 11 is redundant since the <path> element on line 14 explicitly sets its own fill. The path's fill takes precedence, so the SVG-level fill has no effect here.

🧹 Suggested cleanup
     {...iconProperties(props)}
-    fill={"currentColor"}
   >
     <path
       fill={"currentColor"}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/assets/icons/ReverseProxyIcon.tsx` around lines 11 - 16, Remove the
redundant fill attribute from the outer SVG element in ReverseProxyIcon (the SVG
tag that currently has fill={"currentColor"}); the inner <path> already sets
fill="currentColor" so delete the SVG-level fill to clean up the markup while
leaving the path's fill intact.
src/modules/reverse-proxy/targets/ReverseProxyTargetsTable.tsx (1)

77-77: Remove unnecessary optional chaining on required properties.

The reverseProxy prop and targets property are both required by their type definitions (ReverseProxy interface and component props), so optional chaining (?.) is unnecessary. Change initialPageSize={reverseProxy?.targets?.length} to initialPageSize={reverseProxy.targets.length} for consistency with how targets is accessed directly on line 83 (data={reverseProxy.targets}).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/targets/ReverseProxyTargetsTable.tsx` at line 77,
The component uses optional chaining on a required prop: change the prop usage
in the ReverseProxyTargetsTable component so initialPageSize reads
reverseProxy.targets.length instead of reverseProxy?.targets?.length; update the
reference tied to initialPageSize (and ensure consistency with the existing
data={reverseProxy.targets} usage) so you remove unnecessary optional chaining
on reverseProxy and its targets property.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/reverse-proxy/domain/useReverseProxyDomain.ts`:
- Around line 79-104: The parsed domain is only computed on first render (parsed
from parseDomain(reverseProxy.domain, domains)) so subdomain and baseDomain
never update when domains arrives; fix by recomputing parsed when domains or
reverseProxy.domain change (e.g., move parseDomain into a useMemo or recompute
in a useEffect) and update the state via setSubdomain and setBaseDomain when
parsed changes; reference parseDomain, parsed, reverseProxy.domain, domains,
subdomain/setSubdomain and baseDomain/setBaseDomain to locate where to add the
useMemo/useEffect so the derived values reflect the latest domains instead of
sticking to the initial fallback.
- Around line 115-122: isClusterConnected currently compares
reverseProxy.proxy_cluster to d.domain and can return undefined while domains
load; change the comparison to d.target_cluster (i.e., match
reverseProxy.proxy_cluster against d.target_cluster) and coerce the result to a
real boolean (use Boolean(...) or !!) so isClusterConnected always returns
true/false; update the useMemo that references reverseProxy?.proxy_cluster,
domains, ReverseProxyDomainType.FREE and domains?.some(...) accordingly.

In `@src/modules/reverse-proxy/events/ReverseProxyEventsBytesCell.tsx`:
- Around line 19-27: The two byte rows (the elements rendering
ArrowDownIcon/ArrowUpIcon and formatBytes(event.bytes_download ?? 0) /
formatBytes(event.bytes_upload ?? 0)) lack accessible labels; update the JSX in
ReverseProxyEventsBytesCell to include explicit text like "Download" and
"Upload" or add screen-reader-only spans (e.g., <span
className="sr-only">Download:</span> and <span
className="sr-only">Upload:</span>) next to the icons or values so assistive
tech can distinguish bytes_download and bytes_upload for ArrowDownIcon and
ArrowUpIcon rows while preserving the visual layout.

In `@src/modules/reverse-proxy/events/ReverseProxyEventsRequestCell.tsx`:
- Around line 33-37: The code is rebuilding L4 endpoints using the current
service.listen_port (listenPort), which causes historical events to show updated
ports; change hostWithPort/fullUrl construction to rely only on event-owned
data: use event.port (if provided) to append `:${event.port}` when isL4,
otherwise fall back to event.host without synthesizing service.listen_port;
remove use of service.listen_port in hostWithPort and fullUrl (also update the
similar logic around the block handling lines 52-57) so past events reflect
their original data.

In `@src/modules/reverse-proxy/ReverseProxyModal.tsx`:
- Around line 352-357: The timeout string stored in timeoutOption is being
written directly into opts[request_timeout|session_idle_timeout] without
validation; reuse the shared duration validator used by ReverseProxyTargetModal
(the validator invoked by useReverseProxyTargetOptions) to validate
timeoutOption before assigning it to opts, surface any validation error via the
same error prop used by errorTooltip (instead of only errorTooltip) so the
submit is blocked when the validator reports an error, and apply the same change
to the other occurrence around lines 564-571 (the second timeout assignment) so
malformed durations are rejected client-side.

In `@src/modules/reverse-proxy/ReverseProxyServiceModeSelector.tsx`:
- Around line 67-74: The isL4Supported check incorrectly treats undefined
differently from false; change its definition to use a strict boolean check
(e.g. const isL4Supported = !!domain?.supports_custom_ports or
domain?.supports_custom_ports === true) so explicit false prevents L4 modes;
update the useEffect that uses isL4Supported (and the other occurrence around
isL4ServiceMode/onChange/ServiceMode.HTTP at lines ~100-104) to rely on this
boolean value so unsupported domains reset the selected mode back to
ServiceMode.HTTP.

In `@src/modules/reverse-proxy/table/ReverseProxyClusterCell.tsx`:
- Around line 57-74: The render currently calls isNetBirdHosted() directly (in
ReverseProxyClusterCell.jsx/tsx), which accesses window and breaks static
builds; guard that runtime-only call by checking typeof window !== "undefined"
before invoking isNetBirdHosted(), e.g., compute a boolean like isHosted only
when window exists (use a useMemo/useState + useEffect or inline typeof guard)
and then use that boolean in the JSX branches instead of calling
isNetBirdHosted() during server-side/static render. Ensure all references in the
JSX (the ternary that reads isNetBirdHosted()) use this safe variable so no
window access happens at build time.

In `@src/modules/reverse-proxy/targets/ReverseProxyAddressInput.tsx`:
- Around line 11-16: The validation allows subnet hosts before the resource's
CIDR is loaded because resourceAddress can be empty; update the logic in
ReverseProxyAddressInput (the useMemo/resolution of resourceAddress and the
functions isCidrRange / isValidCidrHost / cidrInfo) to require a non-empty
resourceAddress (resolved CIDR) before treating a target as a SUBNET host — if
resourceAddress is not yet available and target.type === 'SUBNET' return false
from the membership/validation checks (or alternatively accept an injected
initialResourceAddress parameter into the hook and use that for immediate
membership checks) so you never fall back to cidr.isValidAddress(target.host)
without the correct subnet context.

---

Outside diff comments:
In `@src/components/DeviceCard.tsx`:
- Around line 28-32: The useMemo that computes descriptionText references
resource?.address but omits resource from its dependency array; update the
dependency list for the useMemo that returns descriptionText to include resource
(e.g., [description, address, device, resource]) so descriptionText recalculates
when resource changes, ensuring the computed value stays current.

---

Nitpick comments:
In `@src/assets/icons/ReverseProxyIcon.tsx`:
- Around line 11-16: Remove the redundant fill attribute from the outer SVG
element in ReverseProxyIcon (the SVG tag that currently has
fill={"currentColor"}); the inner <path> already sets fill="currentColor" so
delete the SVG-level fill to clean up the markup while leaving the path's fill
intact.

In `@src/modules/reverse-proxy/targets/ReverseProxyTargetsTable.tsx`:
- Line 77: The component uses optional chaining on a required prop: change the
prop usage in the ReverseProxyTargetsTable component so initialPageSize reads
reverseProxy.targets.length instead of reverseProxy?.targets?.length; update the
reference tied to initialPageSize (and ensure consistency with the existing
data={reverseProxy.targets} usage) so you remove unnecessary optional chaining
on reverseProxy and its targets property.

In `@src/utils/helpers.ts`:
- Around line 266-275: The formatDuration function should defensively handle
negative and NaN inputs: detect if ms is NaN or less than 0 and return "0ms" (or
clamp to 0 before formatting) instead of producing "-500ms" or "NaNh"; update
the logic in formatDuration to validate the input at the top (using
Number.isFinite or Number.isNaN checks and a negative check) and then proceed
with the existing branch formatting so all outputs remain sensible for invalid
inputs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bca7fd80-4d2d-49e8-9407-7a3abdc8dca1

📥 Commits

Reviewing files that changed from the base of the PR and between 65d1949 and aadd083.

📒 Files selected for processing (34)
  • src/assets/icons/ReverseProxyIcon.tsx
  • src/components/DeviceCard.tsx
  • src/components/RadioCard.tsx
  • src/components/Select.tsx
  • src/contexts/ReverseProxiesProvider.tsx
  • src/interfaces/ReverseProxy.ts
  • src/modules/reverse-proxy/ReverseProxyHTTPTargets.tsx
  • src/modules/reverse-proxy/ReverseProxyLayer4Content.tsx
  • src/modules/reverse-proxy/ReverseProxyModal.tsx
  • src/modules/reverse-proxy/ReverseProxyServiceModeSelector.tsx
  • src/modules/reverse-proxy/domain/CustomDomainModal.tsx
  • src/modules/reverse-proxy/domain/CustomDomainVerificationModal.tsx
  • src/modules/reverse-proxy/domain/ReverseProxyDomainInput.tsx
  • src/modules/reverse-proxy/domain/useReverseProxyDomain.ts
  • src/modules/reverse-proxy/events/ReverseProxyEventsBytesCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsDurationCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsRequestCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsStatusCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsTable.tsx
  • src/modules/reverse-proxy/table/ReverseProxyAuthCell.tsx
  • src/modules/reverse-proxy/table/ReverseProxyClusterCell.tsx
  • src/modules/reverse-proxy/table/ReverseProxyNameCell.tsx
  • src/modules/reverse-proxy/table/ReverseProxyStatusCell.tsx
  • src/modules/reverse-proxy/table/ReverseProxyTable.tsx
  • src/modules/reverse-proxy/table/ReverseProxyTargetsCell.tsx
  • src/modules/reverse-proxy/table/ReverseProxyTypeCell.tsx
  • src/modules/reverse-proxy/targets/ReverseProxyAddressInput.tsx
  • src/modules/reverse-proxy/targets/ReverseProxyTargetDevice.tsx
  • src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx
  • src/modules/reverse-proxy/targets/ReverseProxyTargetSelector.tsx
  • src/modules/reverse-proxy/targets/ReverseProxyTargetsTable.tsx
  • src/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetActionCell.tsx
  • src/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTable.tsx
  • src/utils/helpers.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/modules/reverse-proxy/table/ReverseProxyTargetsCell.tsx
  • src/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTable.tsx
  • src/modules/reverse-proxy/table/ReverseProxyNameCell.tsx
  • src/modules/reverse-proxy/table/ReverseProxyTable.tsx

Comment thread src/modules/reverse-proxy/domain/useReverseProxyDomain.ts
Comment thread src/modules/reverse-proxy/domain/useReverseProxyDomain.ts
Comment thread src/modules/reverse-proxy/events/ReverseProxyEventsBytesCell.tsx
Comment on lines +33 to +37
const listenPort = service?.listen_port;

const hostWithPort =
isL4 && listenPort ? `${event.host}:${listenPort}` : event.host || "-";
const fullUrl = isL4 ? hostWithPort : `${event.host}${event.path}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't reconstruct past event ports from the current service config.

For L4 rows, the displayed/copied endpoint is built with service.listen_port, not the event itself. If the service port is edited later, older events will show the new port, which makes the event log inaccurate. Prefer event-owned data only here; if the backend does not send the original port, fall back to event.host instead of synthesizing :${listenPort}.

Also applies to: 52-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/events/ReverseProxyEventsRequestCell.tsx` around
lines 33 - 37, The code is rebuilding L4 endpoints using the current
service.listen_port (listenPort), which causes historical events to show updated
ports; change hostWithPort/fullUrl construction to rely only on event-owned
data: use event.port (if provided) to append `:${event.port}` when isL4,
otherwise fall back to event.host without synthesizing service.listen_port;
remove use of service.listen_port in hostWithPort and fullUrl (also update the
similar logic around the block handling lines 52-57) so past events reflect
their original data.

Comment thread src/modules/reverse-proxy/ReverseProxyModal.tsx
Comment thread src/modules/reverse-proxy/ReverseProxyServiceModeSelector.tsx Outdated
Comment thread src/modules/reverse-proxy/table/ReverseProxyClusterCell.tsx
Comment thread src/modules/reverse-proxy/targets/ReverseProxyAddressInput.tsx
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 18, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (5)
src/modules/reverse-proxy/ReverseProxyServiceModeSelector.tsx (1)

67-74: ⚠️ Potential issue | 🟠 Major

Use the boolean value of supports_custom_ports.

domain?.supports_custom_ports !== undefined treats false as supported. Domains that explicitly disallow custom ports still expose TCP/TLS/UDP and won't get reset back to HTTP.

Suggested fix
-  const isL4Supported = domain?.supports_custom_ports !== undefined;
+  const isL4Supported = domain?.supports_custom_ports === true;

Also applies to: 100-104

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/ReverseProxyServiceModeSelector.tsx` around lines
67 - 74, The code treats domain?.supports_custom_ports !== undefined as support
which incorrectly treats false as supported; update the isL4Supported check to
use the actual boolean (e.g. domain?.supports_custom_ports === true or
Boolean(domain?.supports_custom_ports)) so domains that explicitly disallow
custom ports are not considered L4-supported; locate the isL4Supported
definition in ReverseProxyServiceModeSelector, keep the existing useEffect that
calls isL4ServiceMode(selected) and onChange(ServiceMode.HTTP) but only trigger
that reset when the corrected isL4Supported becomes false.
src/modules/reverse-proxy/events/ReverseProxyEventsRequestCell.tsx (1)

31-37: ⚠️ Potential issue | 🟠 Major

Don't rebuild historical L4 endpoints from the current service port.

Lines 33-37 and 55-57 append service.listen_port, which is today's config, not the port captured with the event. If the service port changes later, old log rows become wrong. Prefer event-owned data only here; until the backend includes the original port, fall back to event.host.

Suggested fix
-export const ReverseProxyEventsUrlCell = ({ event, service }: Props) => {
+export const ReverseProxyEventsUrlCell = ({ event }: Props) => {
   const isL4 = isL4Event(event);
-  const listenPort = service?.listen_port;
-
-  const hostWithPort =
-    isL4 && listenPort ? `${event.host}:${listenPort}` : event.host || "-";
+  const hostWithPort = event.host || "-";
   const fullUrl = isL4 ? hostWithPort : `${event.host}${event.path}`;
@@
       <CopyToClipboardText message={"URL has been copied to your clipboard"}>
         <span className="font-mono text-[0.82rem] whitespace-nowrap">
-          <span className="text-nb-gray-200">{event.host}</span>
-          {isL4 && listenPort && (
-            <span className="text-nb-gray-300">:{listenPort}</span>
-          )}
+          <span className="text-nb-gray-200">{event.host || "-"}</span>
           {!isL4 && (
             <span className="text-nb-gray-300">{event.path}</span>
           )}
         </span>
       </CopyToClipboardText>

Also applies to: 52-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/events/ReverseProxyEventsRequestCell.tsx` around
lines 31 - 37, The code in ReverseProxyEventsUrlCell (functions/variables:
isL4Event, hostWithPort, fullUrl) is appending the current service.listen_port
to L4 events, which rebuilds historical endpoints from today's config; change
the logic to use only event-owned data (e.g., event.port if present) and fall
back to event.host when event.port is missing — remove usage of
service.listen_port in hostWithPort/fullUrl so past events remain accurate.
src/modules/reverse-proxy/events/ReverseProxyEventsBytesCell.tsx (1)

19-27: ⚠️ Potential issue | 🟡 Minor

Label the byte rows for screen readers.

These two rows are distinguished only by arrow direction and color. Assistive tech will read two unlabeled byte values, so users can't tell which one is download vs upload.

Suggested fix
       <div className={"flex gap-2 items-center whitespace-nowrap"}>
         <ArrowDownIcon size={15} className={cn("text-sky-400")} />
+        <span className="sr-only">Download:</span>
         {formatBytes(event.bytes_download ?? 0)}
       </div>
       <div className={"flex gap-2 items-center whitespace-nowrap"}>
         <ArrowUpIcon size={15} className={cn("text-netbird")} />
+        <span className="sr-only">Upload:</span>
         {formatBytes(event.bytes_upload ?? 0)}
       </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/events/ReverseProxyEventsBytesCell.tsx` around
lines 19 - 27, The two byte rows in ReverseProxyEventsBytesCell.tsx (the
ArrowDownIcon/formatBytes(event.bytes_download) and
ArrowUpIcon/formatBytes(event.bytes_upload) lines) are not labelled for
assistive tech; add accessible labels for each row by including a
screen-reader-only text node or aria-label indicating "Download" and "Upload"
respectively (e.g., add a visually-hidden <span> with "Download" before the
download bytes and "Upload" before the upload bytes, or set aria-label on the
row divs) so formatBytes(event.bytes_download) and
formatBytes(event.bytes_upload) are announced with their context.
src/modules/reverse-proxy/ReverseProxyModal.tsx (1)

352-357: ⚠️ Potential issue | 🟠 Major

L4 timeout input lacks client-side validation before submission.

The timeout string is written directly to request_timeout or session_idle_timeout without validation. The Input has errorTooltip={true} but no error prop to display validation errors. Invalid duration formats (not matching Go time.ParseDuration) will only fail after the API request.

Consider reusing the duration validator from useReverseProxyTargetOptions to validate and display errors:

Suggested approach
// Add validation similar to useReverseProxyTargetOptions
const DURATION_RE = /^(\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$/;

const timeoutError = useMemo(() => {
  if (!timeoutOption) return undefined;
  if (!DURATION_RE.test(timeoutOption))
    return 'Invalid duration, use e.g., "10s", "30s", "1m"';
  return undefined;
}, [timeoutOption]);

// Then in the Input:
<Input
  ...
  error={timeoutError}
  errorTooltip={true}
/>

// And block submit:
disabled={!canContinueToSettings || !!timeoutError || ...}

Also applies to: 564-571

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/ReverseProxyModal.tsx` around lines 352 - 357, The
timeout value from timeoutOption is written straight to
request_timeout/session_idle_timeout without client-side validation; add the
same duration validation used in useReverseProxyTargetOptions (e.g.,
DURATION_RE) as a derived timeoutError (useMemo or similar) that returns an
error string when timeoutOption doesn't match the duration regex, pass that
error to the Input via its error prop while keeping errorTooltip={true}, and
prevent form submission by including !!timeoutError in the disabled condition
used for continuing/save; update both places referenced (around timeoutOption
handling and the other block at lines 564-571) so the UI shows the error and
blocks submit prior to calling the API.
src/modules/reverse-proxy/targets/ReverseProxyAddressInput.tsx (1)

45-48: ⚠️ Potential issue | 🟠 Major

CIDR membership check falls back to syntax-only validation when resource is loading.

When target.type === SUBNET but resources hasn't loaded yet, resourceAddress is empty, making cidrInfo null. The expression then falls back to cidr.isValidAddress(target.host), which only checks IP syntax—not subnet membership. This allows any valid IP to pass validation until the resource loads.

Consider returning false until resourceAddress is available for subnet targets:

Suggested fix
  const isValidCidrHost =
    !isCidrRange ||
+   (isCidrRange && !resourceAddress) ? false :
    (!!target?.host &&
      (cidrInfo ? isHostInCidrRange : cidr.isValidAddress(target.host)));

Or more clearly:

+ const isValidCidrHost = useMemo(() => {
+   if (!isCidrRange) return true;
+   if (!resourceAddress) return false; // Still loading
+   if (!target?.host) return false;
+   return cidrInfo ? isHostInCidrRange : false;
+ }, [isCidrRange, resourceAddress, target?.host, cidrInfo, isHostInCidrRange]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/targets/ReverseProxyAddressInput.tsx` around lines
45 - 48, The validation currently lets a syntactically valid IP pass when
target.type === SUBNET and cidrInfo is null (resources still loading); update
the logic in isValidCidrHost so that if isCidrRange is true and target?.type ===
SUBNET and cidrInfo (or resourceAddress) is not available, the expression
returns false (do not fall back to cidr.isValidAddress); otherwise when cidrInfo
exists use isHostInCidrRange, and for non-subnet CIDR ranges keep the existing
cidr.isValidAddress behavior.
🧹 Nitpick comments (3)
src/modules/reverse-proxy/table/ReverseProxyTypeCell.tsx (1)

54-67: Consider combining identical TCP/UDP icon cases.

Both ServiceMode.TCP and ServiceMode.UDP render the same ArrowRightFromLineIcon. Combining them improves maintainability.

♻️ Combine cases
     case ServiceMode.TLS:
       return <LockKeyhole size={size} className={cn("shrink-0", className)} />;
     case ServiceMode.TCP:
-      return (
-        <ArrowRightFromLineIcon
-          size={size}
-          className={cn("shrink-0", className)}
-        />
-      );
     case ServiceMode.UDP:
       return (
         <ArrowRightFromLineIcon
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/table/ReverseProxyTypeCell.tsx` around lines 54 -
67, The TCP and UDP branches in ReverseProxyTypeCell both render the same
ArrowRightFromLineIcon; consolidate them by combining ServiceMode.TCP and
ServiceMode.UDP into a single case (or use a grouped conditional) that returns
the ArrowRightFromLineIcon with the existing props (size and className via cn),
removing the duplicated JSX to improve maintainability.
src/assets/icons/ReverseProxyIcon.tsx (1)

11-16: Minor redundancy: fill specified on both SVG and path.

The fill="currentColor" on the <svg> element (line 11) is inherited by child elements, making the duplicate on <path> (line 14) redundant. This works correctly but could be simplified.

♻️ Optional cleanup
       fill={"currentColor"}
     >
-      <path
-        fill={"currentColor"}
-        d="M11.4488 2.1499..."
+      <path
+        d="M11.4488 2.1499..."
       />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/assets/icons/ReverseProxyIcon.tsx` around lines 11 - 16, In
ReverseProxyIcon, the fill="currentColor" is duplicated on both the <svg>
element and the child <path>; remove the redundant fill attribute from the
<path> (keep the single fill on the <svg>) so the path inherits currentColor and
the JSX is simplified while preserving the same rendering.
src/modules/reverse-proxy/ReverseProxyHTTPTargets.tsx (1)

138-147: Redundant className in conditional.

The cn("w-full mt-1", targets?.length > 0 && "mt-1") always applies mt-1 regardless of the condition. If different margins were intended, adjust the logic; otherwise, simplify.

♻️ Simplify className
       <Button
         variant="dotted"
-        className={cn("w-full mt-1", targets?.length > 0 && "mt-1")}
+        className="w-full mt-1"
         size="sm"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/reverse-proxy/ReverseProxyHTTPTargets.tsx` around lines 138 -
147, The className expression on the Button is redundant because cn("w-full
mt-1", targets?.length > 0 && "mt-1") always applies "mt-1"; update the Button's
className (in ReverseProxyHTTPTargets.tsx) to a single, non-redundant
value—e.g., replace the cn(...) call with cn("w-full mt-1") if the margin should
always be mt-1, or adjust the conditional branch to return a different class
when targets?.length > 0 (for example "mt-2") if a different margin was
intended; modify the cn usage and keep onClick={onAddTarget} and disabled as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/reverse-proxy/domain/CustomDomainModal.tsx`:
- Around line 103-131: The component calls isNetBirdHosted() (which reads
window.location.hostname) directly and must be made safe for server
environments; update both usages in CustomDomainModal (and also apply the same
guard to CustomDomainVerificationModal mentioned in the review) to check typeof
window !== "undefined" && isNetBirdHosted() before rendering the host-specific
Callout, or alternatively add a "use client" directive at the top of the
component file if the component is intended to be client-only; ensure the unique
symbol isNetBirdHosted and the component names CustomDomainModal and
CustomDomainVerificationModal are the targets of this change.

In `@src/modules/reverse-proxy/table/ReverseProxyStatusCell.tsx`:
- Around line 35-41: The HTTP rendering branch in ReverseProxyStatusCell.tsx
currently doesn't consider the failure flags so services with errors show as
pending; update the HTTP path to consume the same computed booleans (hasError
and isTunnelNotCreated) and also include a check for
ReverseProxyStatus.CERTIFICATE_FAILED (using meta?.status and
dataRef.current?.meta?.status like the L4 branch does) so the HTTP badges render
the appropriate error/failure (red) and certificate-failed states instead of
falling through to the yellow pending badges; apply the same adjustments to the
other status-rendering code in the file that mirrors lines 61-137.

In `@src/modules/reverse-proxy/table/ReverseProxyTargetsCell.tsx`:
- Around line 20-31: The L4 render path in isL4Mode(reverseProxy.mode) assumes
reverseProxy.targets[0] exists; guard the code that computes const target =
reverseProxy?.targets?.[0] and the subsequent address calculation so it first
checks for a missing target and returns a safe fallback (e.g., a
placeholder/null cell) instead of attempting to access host/port; update the
branch that returns <ReverseProxyTargetDevice ...> to only render that component
when target is defined (otherwise return the fallback) and keep the rest of the
logic (address construction and wrapperClassName) inside that guarded block so
rendering never throws when target is undefined.

In `@src/modules/reverse-proxy/table/ReverseProxyTypeCell.tsx`:
- Around line 15-20: In the useMemo that computes serviceModeLabel (the
serviceModeLabel constant in ReverseProxyTypeCell.tsx), guard the SERVICE_MODES
lookup against invalid reverseProxy.mode values by checking that
SERVICE_MODES?.[reverseProxy.mode] exists before accessing .label; if it doesn't
exist, return a safe fallback (e.g., "HTTP/S" or a default label) so that
SERVICE_MODES?.[reverseProxy.mode].label cannot throw when reverseProxy.mode is
unexpected.

In `@src/utils/helpers.ts`:
- Around line 266-273: The formatDuration function currently formats malformed
numeric inputs into "NaN" or "Infinity" strings; before any computations in
formatDuration, validate the input with Number.isFinite(ms) (and optionally ms
>= 0) and return a safe default like "0ms" for non-finite or negative values so
the subsequent checks (ms < 1000, ms < 60000, etc.) never produce NaN/Infinity
outputs.

---

Duplicate comments:
In `@src/modules/reverse-proxy/events/ReverseProxyEventsBytesCell.tsx`:
- Around line 19-27: The two byte rows in ReverseProxyEventsBytesCell.tsx (the
ArrowDownIcon/formatBytes(event.bytes_download) and
ArrowUpIcon/formatBytes(event.bytes_upload) lines) are not labelled for
assistive tech; add accessible labels for each row by including a
screen-reader-only text node or aria-label indicating "Download" and "Upload"
respectively (e.g., add a visually-hidden <span> with "Download" before the
download bytes and "Upload" before the upload bytes, or set aria-label on the
row divs) so formatBytes(event.bytes_download) and
formatBytes(event.bytes_upload) are announced with their context.

In `@src/modules/reverse-proxy/events/ReverseProxyEventsRequestCell.tsx`:
- Around line 31-37: The code in ReverseProxyEventsUrlCell (functions/variables:
isL4Event, hostWithPort, fullUrl) is appending the current service.listen_port
to L4 events, which rebuilds historical endpoints from today's config; change
the logic to use only event-owned data (e.g., event.port if present) and fall
back to event.host when event.port is missing — remove usage of
service.listen_port in hostWithPort/fullUrl so past events remain accurate.

In `@src/modules/reverse-proxy/ReverseProxyModal.tsx`:
- Around line 352-357: The timeout value from timeoutOption is written straight
to request_timeout/session_idle_timeout without client-side validation; add the
same duration validation used in useReverseProxyTargetOptions (e.g.,
DURATION_RE) as a derived timeoutError (useMemo or similar) that returns an
error string when timeoutOption doesn't match the duration regex, pass that
error to the Input via its error prop while keeping errorTooltip={true}, and
prevent form submission by including !!timeoutError in the disabled condition
used for continuing/save; update both places referenced (around timeoutOption
handling and the other block at lines 564-571) so the UI shows the error and
blocks submit prior to calling the API.

In `@src/modules/reverse-proxy/ReverseProxyServiceModeSelector.tsx`:
- Around line 67-74: The code treats domain?.supports_custom_ports !== undefined
as support which incorrectly treats false as supported; update the isL4Supported
check to use the actual boolean (e.g. domain?.supports_custom_ports === true or
Boolean(domain?.supports_custom_ports)) so domains that explicitly disallow
custom ports are not considered L4-supported; locate the isL4Supported
definition in ReverseProxyServiceModeSelector, keep the existing useEffect that
calls isL4ServiceMode(selected) and onChange(ServiceMode.HTTP) but only trigger
that reset when the corrected isL4Supported becomes false.

In `@src/modules/reverse-proxy/targets/ReverseProxyAddressInput.tsx`:
- Around line 45-48: The validation currently lets a syntactically valid IP pass
when target.type === SUBNET and cidrInfo is null (resources still loading);
update the logic in isValidCidrHost so that if isCidrRange is true and
target?.type === SUBNET and cidrInfo (or resourceAddress) is not available, the
expression returns false (do not fall back to cidr.isValidAddress); otherwise
when cidrInfo exists use isHostInCidrRange, and for non-subnet CIDR ranges keep
the existing cidr.isValidAddress behavior.

---

Nitpick comments:
In `@src/assets/icons/ReverseProxyIcon.tsx`:
- Around line 11-16: In ReverseProxyIcon, the fill="currentColor" is duplicated
on both the <svg> element and the child <path>; remove the redundant fill
attribute from the <path> (keep the single fill on the <svg>) so the path
inherits currentColor and the JSX is simplified while preserving the same
rendering.

In `@src/modules/reverse-proxy/ReverseProxyHTTPTargets.tsx`:
- Around line 138-147: The className expression on the Button is redundant
because cn("w-full mt-1", targets?.length > 0 && "mt-1") always applies "mt-1";
update the Button's className (in ReverseProxyHTTPTargets.tsx) to a single,
non-redundant value—e.g., replace the cn(...) call with cn("w-full mt-1") if the
margin should always be mt-1, or adjust the conditional branch to return a
different class when targets?.length > 0 (for example "mt-2") if a different
margin was intended; modify the cn usage and keep onClick={onAddTarget} and
disabled as-is.

In `@src/modules/reverse-proxy/table/ReverseProxyTypeCell.tsx`:
- Around line 54-67: The TCP and UDP branches in ReverseProxyTypeCell both
render the same ArrowRightFromLineIcon; consolidate them by combining
ServiceMode.TCP and ServiceMode.UDP into a single case (or use a grouped
conditional) that returns the ArrowRightFromLineIcon with the existing props
(size and className via cn), removing the duplicated JSX to improve
maintainability.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c82c1066-bc23-4c6b-93a0-94a242515988

📥 Commits

Reviewing files that changed from the base of the PR and between 65d1949 and aadd083.

📒 Files selected for processing (34)
  • src/assets/icons/ReverseProxyIcon.tsx
  • src/components/DeviceCard.tsx
  • src/components/RadioCard.tsx
  • src/components/Select.tsx
  • src/contexts/ReverseProxiesProvider.tsx
  • src/interfaces/ReverseProxy.ts
  • src/modules/reverse-proxy/ReverseProxyHTTPTargets.tsx
  • src/modules/reverse-proxy/ReverseProxyLayer4Content.tsx
  • src/modules/reverse-proxy/ReverseProxyModal.tsx
  • src/modules/reverse-proxy/ReverseProxyServiceModeSelector.tsx
  • src/modules/reverse-proxy/domain/CustomDomainModal.tsx
  • src/modules/reverse-proxy/domain/CustomDomainVerificationModal.tsx
  • src/modules/reverse-proxy/domain/ReverseProxyDomainInput.tsx
  • src/modules/reverse-proxy/domain/useReverseProxyDomain.ts
  • src/modules/reverse-proxy/events/ReverseProxyEventsBytesCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsDurationCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsRequestCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsStatusCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsTable.tsx
  • src/modules/reverse-proxy/table/ReverseProxyAuthCell.tsx
  • src/modules/reverse-proxy/table/ReverseProxyClusterCell.tsx
  • src/modules/reverse-proxy/table/ReverseProxyNameCell.tsx
  • src/modules/reverse-proxy/table/ReverseProxyStatusCell.tsx
  • src/modules/reverse-proxy/table/ReverseProxyTable.tsx
  • src/modules/reverse-proxy/table/ReverseProxyTargetsCell.tsx
  • src/modules/reverse-proxy/table/ReverseProxyTypeCell.tsx
  • src/modules/reverse-proxy/targets/ReverseProxyAddressInput.tsx
  • src/modules/reverse-proxy/targets/ReverseProxyTargetDevice.tsx
  • src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx
  • src/modules/reverse-proxy/targets/ReverseProxyTargetSelector.tsx
  • src/modules/reverse-proxy/targets/ReverseProxyTargetsTable.tsx
  • src/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetActionCell.tsx
  • src/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTable.tsx
  • src/utils/helpers.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/modules/reverse-proxy/table/ReverseProxyAuthCell.tsx
  • src/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTable.tsx
  • src/contexts/ReverseProxiesProvider.tsx

Comment thread src/modules/reverse-proxy/domain/CustomDomainModal.tsx
Comment thread src/modules/reverse-proxy/table/ReverseProxyStatusCell.tsx
Comment thread src/modules/reverse-proxy/table/ReverseProxyTargetsCell.tsx
Comment thread src/modules/reverse-proxy/table/ReverseProxyTypeCell.tsx
Comment thread src/utils/helpers.ts Outdated
@heisbrot heisbrot merged commit aff2365 into main Mar 18, 2026
4 checks passed
@heisbrot heisbrot deleted the feature/reverse-proxy branch March 18, 2026 16:43
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.

2 participants