Skip to content

fix(network): remote LAN-share UI actually works (same-origin API + safe clipboard) - #171

Merged
debpalash merged 2 commits into
mainfrom
fix/remote-share-api-origin
May 30, 2026
Merged

fix(network): remote LAN-share UI actually works (same-origin API + safe clipboard)#171
debpalash merged 2 commits into
mainfrom
fix/remote-share-api-origin

Conversation

@debpalash

@debpalash debpalash commented May 30, 2026

Copy link
Copy Markdown
Owner

Opening the LAN-share URL on another device showed a wall of CORS errors + a clipboard crash. Two compounding remote-only bugs:

  1. Same-origin API — the SPA served by the share listener on :5050 had its API client hardcoded to :3900, which is cross-origin (CORS-blocked) and loopback-only/unreachable from another machine. The share listener serves the same app+API, so the remote SPA must use its own origin. _resolveApiBase: Tauri→127.0.0.1; vite-dev→:3900 (CORS-allowed); else (share listener / Docker / prod build)→window.location.origin. +6 unit tests.
  2. Safe clipboardnavigator.clipboard is secure-context-only; over plain HTTP it's undefined, so unguarded writeText(...) threw and crashed copy buttons. New copyText util (clipboard API → hidden-textarea/execCommand fallback), routed through all 9 call sites. +2 tests.

Verified: typecheck:ci ✓, test:legacy ✓, vitest 103/103 ✓, build ✓. This is the fix that makes the (real, already-working backend) sharing usable end-to-end from a phone/second machine.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced clipboard functionality with improved fallback support for better compatibility across different environments and browser contexts.
  • Tests

    • Added comprehensive test coverage for API base URL resolution.
    • Added test coverage for clipboard operation reliability.

Review Change Stack

debpalash and others added 2 commits May 30, 2026 17:29
… :3900

When a device opens the LAN-share URL, the SPA is served by the share listener
on :5050 but client.ts hardcoded the API to :3900 — cross-origin (CORS-blocked)
AND loopback-only/unreachable from another machine, so every fetch failed. The
share listener serves the same app+API, so the remote SPA must hit its OWN
origin. _resolveApiBase: Tauri→127.0.0.1; vite-dev→:3900 (CORS-allowed); else
(share listener / docker / prod build)→window.location.origin. +6 unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
navigator.clipboard is secure-context-only (https/localhost); on a LAN-shared
instance at http://<ip>:<port> it's undefined, so unguarded
navigator.clipboard.writeText(...) threw 'Cannot read properties of undefined
(reading writeText)' — crashing copy buttons on remote devices. Add a copyText
util (clipboard API when available, hidden-textarea + execCommand fallback
otherwise) and route all 9 call sites through it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR refactors API base URL resolution into a testable pure function and introduces a centralized clipboard copy utility with fallback support, replacing direct navigator.clipboard calls across nine UI components for better robustness across insecure and edge-case contexts.

Changes

API Base URL Resolution

Layer / File(s) Summary
API resolution function
frontend/src/api/client.ts
Pure _resolveApiBase(env, win) function selects backend base URL from VITE_API_URL override, Tauri local sidecar, Vite dev mode with port mapping, and falls back to window.location.origin; API export updated to use this resolver.
API resolution tests
frontend/src/api/client.apibase.test.ts
Six test cases validate URL selection across non-Tauri browser requests, Tauri webview routing, Vite dev mode, explicit override precedence, SSR/no-window fallback, and dynamic VITE_API_PORT configuration.

Clipboard Copy Utility

Layer / File(s) Summary
Clipboard copy utility
frontend/src/utils/copyText.js
copyText(text) utility attempts async Clipboard API when available, falls back to hidden textarea with document.execCommand('copy') for insecure contexts or permission failures, returns boolean success.
Clipboard copy tests
frontend/src/utils/copyText.test.js
Vitest suite verifies Clipboard API usage in secure contexts and execCommand('copy') fallback when navigator.clipboard is absent, with proper mock cleanup.
Component clipboard integration
frontend/src/components/BootstrapSplash.jsx, frontend/src/components/CaptureWidget.jsx, frontend/src/components/LogsFooter.jsx, frontend/src/components/NetworkToggle.jsx, frontend/src/components/settings/SharingPanel.jsx, frontend/src/pages/DubTab.jsx, frontend/src/pages/Projects.jsx, frontend/src/pages/Settings.jsx, frontend/src/pages/Transcriptions.jsx
Nine components import copyText and replace direct navigator.clipboard.writeText() calls in log, diagnostic, transcription, and share-URL copy handlers, preserving existing success/error toast flows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • debpalash/OmniVoice-Studio#123: Earlier change to API base URL resolution logic that selected between 127.0.0.1 and window.location-derived hosts with VITE_API_URL override precedence.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers both bugs fixed, the technical solutions, and verification status; however, it lacks explicit checkbox completion against the provided template sections (Summary, Changes, Type, Testing, Checklist). Fill out the PR template explicitly: add a bulleted changes list, mark the bug-fix checkbox, detail testing steps, and confirm all checklist items are addressed.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the two main changes: same-origin API resolution for remote LAN-share and safe clipboard fallback, matching the file changes across both utilities and their integration points.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/remote-share-api-origin

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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.

* page updates in realtime without requiring a shared store.
*/
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { copyText } from "../utils/copyText";
@greptile-apps

greptile-apps Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two compounding bugs that broke LAN-share for remote devices: the SPA's API client was hardcoded to :3900 (cross-origin and loopback-only), and navigator.clipboard is undefined over plain HTTP, crashing all copy buttons.

  • client.ts: _resolveApiBase now correctly routes Tauri→127.0.0.1, Vite dev→:3900, and anything served by the backend itself (share listener / Docker / prod) → window.location.origin.
  • copyText.js: New utility wraps the Clipboard API with a textarea/execCommand fallback for non-secure contexts, and is wired into all 9 copy call-sites across the app.
  • Transcriptions.jsx introduces a naming collision: the imported copyText is shadowed by a local const copyText = useCallback(...) that calls copyText(text) within its own body — causing infinite recursion on every copy action in that page.

Confidence Score: 2/5

Not safe to merge — the Transcriptions copy handler is broken by a name-shadowing bug introduced in this PR.

The migration in Transcriptions.jsx imports copyText but then declares a local const copyText = useCallback(...) that calls copyText(text) inside its own body. Because const declarations shadow the module import in the same scope, the callback recurses into itself on every invocation, producing a stack overflow whenever a user copies a transcription. This is a crash introduced by the PR on the Transcriptions page copy path.

frontend/src/pages/Transcriptions.jsx — the local copyText callback must be renamed (e.g. handleCopy) so the imported utility is reachable.

Important Files Changed

Filename Overview
frontend/src/pages/Transcriptions.jsx Imports copyText but then declares a local const copyText that shadows the import, causing infinite recursion on every copy action.
frontend/src/api/client.ts Refactors API base URL resolution into a testable _resolveApiBase function; correctly routes Tauri→loopback, dev→:3900, and LAN-served→window.location.origin.
frontend/src/utils/copyText.js New clipboard utility with secure-context guard and textarea/execCommand fallback; correctly handles undefined navigator.clipboard over plain HTTP.
frontend/src/components/NetworkToggle.jsx Swaps navigator.clipboard for copyText utility; toast fires without awaiting the promise (pre-existing pattern, not a regression).
frontend/src/components/settings/SharingPanel.jsx Same un-awaited copyText pattern as NetworkToggle; otherwise a clean migration to the safe clipboard utility.
frontend/src/api/client.apibase.test.ts Adds 6 unit tests covering all _resolveApiBase branches including the LAN-share same-origin case.
frontend/src/utils/copyText.test.js Tests secure-context clipboard path and execCommand fallback; correctly mocks navigator.clipboard absence.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[_resolveApiBase called] --> B{VITE_API_URL set?}
    B -- yes --> C[Return VITE_API_URL override]
    B -- no --> D{window exists?}
    D -- no SSR --> E[Return 127.0.0.1:port]
    D -- yes --> F{window.__TAURI__?}
    F -- yes --> G[Return 127.0.0.1:port local sidecar]
    F -- no --> H{import.meta.env.DEV?}
    H -- yes --> I[Return hostname:port localhost:3900 in dev]
    H -- no --> J[Return window.location.origin LAN share / Docker / prod]

    K[copyText called] --> L{navigator.clipboard available?}
    L -- yes --> M[navigator.clipboard.writeText]
    M -- success --> N[return true]
    M -- throws --> O[fall through to execCommand]
    L -- no plain HTTP --> O
    O --> P[hidden textarea + execCommand copy]
    P --> Q[return ok boolean]
Loading

Comments Outside Diff (1)

  1. frontend/src/pages/Transcriptions.jsx, line 77-82 (link)

    P0 Infinite recursion — stack overflow on every copy

    The local const copyText = useCallback(...) shadows the imported copyText from ../utils/copyText. Inside the callback body, copyText(text) resolves to the same local variable, so it calls itself instead of the utility — causing a RangeError: Maximum call stack size exceeded whenever a user tries to copy a transcription. The import is never reached.

    Fix in Claude Code

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix(ui): safe clipboard copy over plain ..." | Re-trigger Greptile

};

const copy = (text) => { navigator.clipboard?.writeText(text); toast.success('Copied'); };
const copy = (text) => { copyText(text); toast.success('Copied'); };

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.

P2 toast.success fires before copy resolves

copyText(text) returns a Promise but it is not awaited, so toast.success('Copied') fires immediately regardless of whether the copy succeeds or fails. The same pattern also exists in SharingPanel.jsx. The previous navigator.clipboard?.writeText was equally un-awaited, so this isn't a regression — but the new utility makes it trivial to await and gate the toast on success.

Fix in Claude Code

@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)
frontend/src/pages/Transcriptions.jsx (1)

77-82: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: name shadowing causes infinite recursion — the imported copyText is never called.

The local const copyText (Line 77) shadows the imported copyText from ../utils/copyText (Line 11). Inside the callback body, copyText(text) resolves to the local binding itself, so invoking it recurses until a stack overflow (Maximum call stack size exceeded). Every copy action in this page (Line 210) will throw, and the new utility is never reached — directly defeating this PR's clipboard fix.

Alias the import to break the shadowing:

🐛 Proposed fix
-import { copyText } from "../utils/copyText";
+import { copyText as copyTextUtil } from "../utils/copyText";
   const copyText = useCallback((text) => {
-    copyText(text).then(
+    copyTextUtil(text).then(
       () => toast.success(t('transcriptions.copied')),
       () => toast.error(t('transcriptions.copy_failed'))
     );
   }, [t]);
🤖 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 `@frontend/src/pages/Transcriptions.jsx` around lines 77 - 82, The local const
copyText defined with useCallback shadows the imported copyText utility and
causes infinite recursion; rename one of them (recommended: alias the import to
copyToClipboard or rename the callback to handleCopyText) and update the
callback body to call the imported function (e.g.,
copyToClipboard(text).then(...)) so the utility from ../utils/copyText is
actually invoked; ensure references elsewhere (e.g., where copy is triggered on
line ~210) use the new callback name if you renamed it.
🤖 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 `@frontend/src/components/NetworkToggle.jsx`:
- Line 51: The helper function copy currently calls the async copyText without
awaiting its Promise, causing toast.success to show even on failure; update the
copy function (the copy helper that calls copyText and toast.success) to await
copyText(text), check the returned boolean, and only call
toast.success('Copied') when it resolves true, otherwise call toast.error(...)
or another failure handler so users see correct feedback.

In `@frontend/src/components/settings/SharingPanel.jsx`:
- Line 154: The copy helper in SharingPanel.jsx currently calls copyText(text)
without awaiting it, so toast.success('Copied') runs even on failure; make the
copy function async, await copyText(text), and move toast.success into the
success path, catching errors to call toast.error (or show appropriate failure
feedback); reference the copy function and the copyText helper and ensure you
handle promise rejection from copyText before showing success.

In `@frontend/src/pages/Projects.jsx`:
- Around line 172-175: The onClick handler currently calls copyText(tr.text ||
'') without awaiting or reporting result; update the handler in Projects.jsx to
await copyText(tr.text || '') and show user feedback: on success call the
project's toast/notification helper (e.g., toast.success or showToast) to
display a "Copied" message, and on failure catch the error and show an error
toast with the error message; reference the onClick handler and the copyText
function so the change is limited to that click callback and uses existing toast
utilities in the component.

---

Outside diff comments:
In `@frontend/src/pages/Transcriptions.jsx`:
- Around line 77-82: The local const copyText defined with useCallback shadows
the imported copyText utility and causes infinite recursion; rename one of them
(recommended: alias the import to copyToClipboard or rename the callback to
handleCopyText) and update the callback body to call the imported function
(e.g., copyToClipboard(text).then(...)) so the utility from ../utils/copyText is
actually invoked; ensure references elsewhere (e.g., where copy is triggered on
line ~210) use the new callback name if you renamed it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 48571aaf-18a4-465d-8b44-134c42311805

📥 Commits

Reviewing files that changed from the base of the PR and between 1a0798f and 27530a0.

📒 Files selected for processing (13)
  • frontend/src/api/client.apibase.test.ts
  • frontend/src/api/client.ts
  • frontend/src/components/BootstrapSplash.jsx
  • frontend/src/components/CaptureWidget.jsx
  • frontend/src/components/LogsFooter.jsx
  • frontend/src/components/NetworkToggle.jsx
  • frontend/src/components/settings/SharingPanel.jsx
  • frontend/src/pages/DubTab.jsx
  • frontend/src/pages/Projects.jsx
  • frontend/src/pages/Settings.jsx
  • frontend/src/pages/Transcriptions.jsx
  • frontend/src/utils/copyText.js
  • frontend/src/utils/copyText.test.js

};

const copy = (text) => { navigator.clipboard?.writeText(text); toast.success('Copied'); };
const copy = (text) => { copyText(text); toast.success('Copied'); };

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 | 🔴 Critical | ⚡ Quick win

Missing await breaks error feedback.

copyText is async and returns Promise<boolean>, but this helper doesn't await it. The success toast will fire immediately, even when the copy fails. Users will see "Copied" when nothing was actually copied to the clipboard.

🔧 Proposed fix
-  const copy = (text) => { copyText(text); toast.success('Copied'); };
+  const copy = async (text) => {
+    const ok = await copyText(text);
+    if (ok) toast.success('Copied');
+    else toast.error('Copy failed');
+  };
📝 Committable suggestion

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

Suggested change
const copy = (text) => { copyText(text); toast.success('Copied'); };
const copy = async (text) => {
const ok = await copyText(text);
if (ok) toast.success('Copied');
else toast.error('Copy failed');
};
🤖 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 `@frontend/src/components/NetworkToggle.jsx` at line 51, The helper function
copy currently calls the async copyText without awaiting its Promise, causing
toast.success to show even on failure; update the copy function (the copy helper
that calls copyText and toast.success) to await copyText(text), check the
returned boolean, and only call toast.success('Copied') when it resolves true,
otherwise call toast.error(...) or another failure handler so users see correct
feedback.

};

const copy = (text) => { navigator.clipboard?.writeText(text); toast.success('Copied'); };
const copy = (text) => { copyText(text); toast.success('Copied'); };

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 | 🔴 Critical | ⚡ Quick win

Missing await breaks error feedback.

Same issue as NetworkToggle.jsx line 51: the helper doesn't await copyText, so the success toast fires immediately regardless of whether the copy succeeded. Users see "Copied" even when the clipboard operation fails.

🔧 Proposed fix
-  const copy = (text) => { copyText(text); toast.success('Copied'); };
+  const copy = async (text) => {
+    const ok = await copyText(text);
+    if (ok) toast.success('Copied');
+    else toast.error('Copy failed');
+  };
📝 Committable suggestion

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

Suggested change
const copy = (text) => { copyText(text); toast.success('Copied'); };
const copy = async (text) => {
const ok = await copyText(text);
if (ok) toast.success('Copied');
else toast.error('Copy failed');
};
🤖 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 `@frontend/src/components/settings/SharingPanel.jsx` at line 154, The copy
helper in SharingPanel.jsx currently calls copyText(text) without awaiting it,
so toast.success('Copied') runs even on failure; make the copy function async,
await copyText(text), and move toast.success into the success path, catching
errors to call toast.error (or show appropriate failure feedback); reference the
copy function and the copyText helper and ensure you handle promise rejection
from copyText before showing success.

Comment on lines 172 to 175
onClick: () => {
navigator.clipboard.writeText(tr.text || '');
copyText(tr.text || '');
},
});

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

Add user feedback for copy action.

The copyText call is not awaited and provides no feedback to the user. If the copy fails (e.g., in an HTTP context where the Clipboard API is unavailable), the user gets no indication. At minimum, add a success toast; ideally handle errors too.

📋 Proposed fix with feedback
-        onClick: () => {
-          copyText(tr.text || '');
-        },
+        onClick: async () => {
+          const ok = await copyText(tr.text || '');
+          if (ok) toast.success('Copied');
+          else toast.error('Copy failed');
+        },
🤖 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 `@frontend/src/pages/Projects.jsx` around lines 172 - 175, The onClick handler
currently calls copyText(tr.text || '') without awaiting or reporting result;
update the handler in Projects.jsx to await copyText(tr.text || '') and show
user feedback: on success call the project's toast/notification helper (e.g.,
toast.success or showToast) to display a "Copied" message, and on failure catch
the error and show an error toast with the error message; reference the onClick
handler and the copyText function so the change is limited to that click
callback and uses existing toast utilities in the component.

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