Skip to content

feat(network): user-configurable backend / LAN-share / UI ports - #163

Merged
debpalash merged 4 commits into
mainfrom
feat/configurable-ports
May 30, 2026
Merged

feat(network): user-configurable backend / LAN-share / UI ports#163
debpalash merged 4 commits into
mainfrom
feat/configurable-ports

Conversation

@debpalash

@debpalash debpalash commented May 30, 2026

Copy link
Copy Markdown
Owner

Lets users adapt OmniVoice to their own networks instead of hardcoded 3900/3901. Env vars are the source of truth (the ports are read at startup by the Rust sidecar before any in-app config loads); Settings shows them and makes the runtime LAN-share port editable.

What changed

  • OMNIVOICE_PORT (backend) was already read by the Rust sidecar; now Python honors it too. Fixes the core bug: network_share.py hardcoded 3900, so a custom backend port left LAN-share + Tailscale still proxying :3900. network_share.backend_port() / share_port_base() are now env-driven.
  • OMNIVOICE_SHARE_PORT (default backend+1) — the LAN-share listener base; editable in Settings (persists via the existing prefs mechanism, applies on next enable).
  • OMNIVOICE_UI_PORT (default 3901) — Vite dev port + CORS origins.
  • Rust↔Python agreement made deterministic: backend.rs now pushes OMNIVOICE_PORT onto the spawned child's env, so Python's backend_port() always equals the --port Rust used — the listener and the LAN-share/Tailscale target can never diverge.
  • /system/info exposes backend_port / share_port_base / ui_port; Settings → Sharing has a Ports subsection (displays backend/UI ports + env-var names + "restart to apply"; LAN-share port editable). set-env validates port keys (numeric, 1024–65535).

Verification (full CI-equivalent, all green locally)

Backend 32 passed · cargo check ✓ · typecheck:ci ✓ · test:legacy 36 ✓ · vitest 91 ✓ · build ✓.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added configurable LAN-share port settings in the Sharing Panel UI
    • System info now displays backend, UI, and LAN-share port values
    • Port configuration validates inputs (must be a valid TCP port: 1024–65535)
  • Tests

    • Extended test coverage for port configuration behavior and validation

Review Change Stack

debpalash and others added 4 commits May 30, 2026 12:45
Single-source the backend port from OMNIVOICE_PORT and derive the LAN-share
base from it (OMNIVOICE_SHARE_PORT override). Previously network_share.py
hardcoded BACKEND_PORT=3900, so a user running the backend on a custom port
got LAN-share/Tailscale pointed at the wrong port.

- network_share: replace BACKEND_PORT constant with backend_port() /
  share_port_base() helpers (env-driven, never-throw fallback to defaults);
  enable() probes from share_port_base().
- tailscale: serve_enable(port=None) defaults to network_share.backend_port().
- main.py: direct-run + --health-check ports and HEALTH_URL read OMNIVOICE_PORT;
  CORS default origins use OMNIVOICE_UI_PORT (default 3901).
- /system/info + SystemInfoResponse: expose backend_port, share_port_base,
  ui_port (both success and never-throw except branches).
- set-env: persist OMNIVOICE_PORT/SHARE_PORT/UI_PORT; validate numeric and
  1024-65535, reject otherwise with 400.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Push OMNIVOICE_PORT=backend_port() onto the spawned Python child's env so
network_share.backend_port() always agrees with the uvicorn --port Rust
passes. Without this, a user-set OMNIVOICE_PORT would move the LAN-share /
Tailscale target while the listener stayed on the Rust-resolved port.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- vite.config.js: dev-server port reads OMNIVOICE_UI_PORT (default 3901).
- SharingPanel: new Ports subsection reads /system/info and displays
  backend_port / ui_port (with their env-var names + restart-to-apply note)
  and makes the LAN-share port editable — Save POSTs OMNIVOICE_SHARE_PORT to
  /system/set-env (persisted), "applies next time you enable sharing".
- Tests: extend SharingPanel.test.jsx for the ports subsection.

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

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This PR introduces environment-driven port configuration for the backend, UI dev server, and LAN-share listener. Port values transition from hardcoded constants to configurable environment variables with safe fallbacks, exposed via the /system/info API endpoint. The POST /system/set-env endpoint validates port numbers to enforce TCP range constraints. Frontend UI adds a "Ports" section in the sharing settings for users to configure the LAN-share listener port.

Changes

Port configuration feature

Layer / File(s) Summary
Port resolution helpers and API contract
backend/services/network_share.py, backend/api/schemas.py, backend/api/routers/system.py, tests/test_network_share.py
New backend_port() and share_port_base() helpers read OMNIVOICE_PORT and OMNIVOICE_SHARE_PORT from environment with fallbacks to 3900 and backend_port+1 respectively. SystemInfoResponse schema extended with three integer port fields. Port configuration keys defined with validation markers. Unit tests verify resolution defaults, environment overrides, and fallback on malformed values.
API endpoints exposing port configuration
backend/api/routers/system.py, tests/test_network_share.py
GET /system/info returns current backend, share-base, and UI port values. POST /system/set-env validates that any OMNIVOICE_*_PORT value is numeric and within 1024–65535 range, rejecting invalid inputs with 400 errors. Tests cover field presence, endpoint validation rules, and out-of-range rejection.
Backend server initialization and CORS
backend/main.py
Application startup reads resolved UI port via _ui_port() for CORS allowed origins; reads backend port via network_share.backend_port() for health-check endpoint and uvicorn listener, with bind host controlled by existing OMNIVOICE_BIND_HOST.
Service and process integration
backend/services/tailscale.py, backend/services/network_share.py, frontend/src-tauri/src/backend.rs, frontend/vite.config.js, tests/test_tailscale_service.py
Tailscale serve_enable() now accepts optional port and resolves to network_share.backend_port() when unspecified. Network-share enable() probes for listener from share_port_base() instead of hardcoded offset. Rust/Tauri spawn adds OMNIVOICE_PORT env variable matching resolved uvicorn port. Vite dev server port reads OMNIVOICE_UI_PORT environment variable. Test verifies tailscale proxy target uses configured backend port.
Frontend port configuration interface
frontend/src/components/settings/SharingPanel.jsx, frontend/src/components/settings/SharingPanel.css, frontend/src/components/settings/SharingPanel.test.jsx
SharingPanel component fetches port values from /system/info on mount, displays backend and UI ports as read-only, provides editable LAN-share port input with save button that validates 1024–65535 range and persists to /system/set-env. New CSS classes style port labels and input fields. Tests verify ports section renders with correct values and environment-variable labels.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(network): user-configurable backend / LAN-share / UI ports' accurately and concisely summarizes the main change—making backend, LAN-share, and UI ports user-configurable instead of hardcoded.
Description check ✅ Passed The PR description covers the key changes, provides context about why the changes were made, explains the verification steps, but lacks explicit coverage of required template sections like Testing checklist items and does not explicitly check template boxes.
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 feat/configurable-ports

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.

@greptile-apps

greptile-apps Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces hardcoded ports 3900/3901 throughout the Python backend, Rust sidecar, and Vite config with env-driven helpers (OMNIVOICE_PORT, OMNIVOICE_SHARE_PORT, OMNIVOICE_UI_PORT), and surfaces a read/edit UI for these ports in Settings → Sharing.

  • network_share.py: BACKEND_PORT constant replaced by backend_port() / share_port_base() env-driven functions; _find_free_port can scan past port 65535 and throw an uncaught OverflowError when OMNIVOICE_SHARE_PORT is set near the maximum.
  • backend.rs: Rust now pushes the resolved OMNIVOICE_PORT into the Python child's env so the listener port and LAN-share/Tailscale proxy target can never diverge.
  • system.py / main.py: Port validation added to set-env; /system/info exposes the three resolved ports; _ui_port() is duplicated in both files.

Confidence Score: 3/5

Safe to merge after fixing the _find_free_port upper-bound issue; all other changes are clean and well-tested.

The _find_free_port function scans a 20-port window starting from the user-supplied OMNIVOICE_SHARE_PORT. Because socket.bind raises OverflowError (not OSError) for port numbers above 65535, any user who sets OMNIVOICE_SHARE_PORT to 65517 or higher and encounters a busy port will get an unhandled crash path. The rest of the PR is solid: Rust→Python port agreement, the new set-env validation, the UI subsection, and the test coverage are all correct.

backend/services/network_share.py — the _find_free_port scan range needs to be capped at 65535.

Important Files Changed

Filename Overview
backend/services/network_share.py Replaces BACKEND_PORT constant with env-driven backend_port()/share_port_base() helpers; _find_free_port() can throw OverflowError when scanning past port 65535.
backend/api/routers/system.py Adds port keys to PERSISTENT_KEYS, validates port range in set-env, and exposes backend_port/share_port_base/ui_port in /system/info; _ui_port() duplicates the same helper already in main.py.
backend/main.py Switches hardcoded 3900 port to network_share.backend_port() in health check and uvicorn.run; CORS origins now use OMNIVOICE_UI_PORT at module-load time.
frontend/src-tauri/src/backend.rs Pushes OMNIVOICE_PORT env var into Python child process so Rust and Python always agree on the listening port; straightforward and correct.
frontend/src/components/settings/SharingPanel.jsx Adds Ports subsection with read-only display for backend/UI ports and an editable LAN-share port input; cancel-safe fetch on mount, optimistic local state update on save.
frontend/vite.config.js Uses OMNIVOICE_UI_PORT env var for Vite dev server port; Number(...)
backend/api/schemas.py Adds backend_port, share_port_base, and ui_port fields to SystemInfoResponse with sensible defaults.
tests/test_network_share.py Adds thorough unit tests for the new port helpers and set-env validation.
tests/test_tailscale_service.py Adds test verifying serve_enable() proxies to the configured OMNIVOICE_PORT rather than hardcoded 3900.

Sequence Diagram

sequenceDiagram
    participant Rust as backend.rs
    participant Python as main.py
    participant NS as network_share.py
    participant TS as tailscale.py
    participant UI as SharingPanel.jsx

    Note over Rust: Reads OMNIVOICE_PORT env var
    Rust->>Python: spawn with OMNIVOICE_PORT in env
    Python->>Python: CORS origins built from _ui_port()
    Python->>Python: "uvicorn.run(port=backend_port())"

    UI->>Python: GET /system/info
    Python->>NS: backend_port(), share_port_base()
    Python-->>UI: backend_port, share_port_base, ui_port

    UI->>Python: POST /system/set-env OMNIVOICE_SHARE_PORT
    Python->>Python: validate 1024-65535, persist to prefs
    Python-->>UI: set: true

    UI->>Python: POST /system/network/enable
    Python->>NS: enable(app)
    NS->>NS: _find_free_port(share_port_base())
    NS->>NS: uvicorn.Server(0.0.0.0, free_port)

    UI->>Python: POST /system/tailscale/enable
    Python->>TS: serve_enable()
    TS->>NS: backend_port()
    TS->>TS: tailscale serve http://127.0.0.1:port
Loading

Comments Outside Diff (1)

  1. backend/services/network_share.py, line 87-88 (link)

    P1 _find_free_port scans range(base, base + 20), but socket.bind raises OverflowError (not OSError) for port numbers above 65535. Because the set-env validator allows values up to 65535, a user who sets OMNIVOICE_SHARE_PORT=65525 and finds all of 65525–65535 in use will trigger an uncaught OverflowError that bypasses the except OSError clause and propagates as a 500 to the enable endpoint. Capping the scan range at 65535 prevents the crash.

    Fix in Claude Code

Fix All in Claude Code

Reviews (1): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment on lines +41 to +53
def _ui_port() -> int:
"""The Vite UI dev-server port, single-sourced from OMNIVOICE_UI_PORT.

Mirrors the resolver in main.py (kept local to avoid importing the app
module). Falls back to 3901 on a missing or malformed value.
"""
raw = os.environ.get("OMNIVOICE_UI_PORT")
if raw is None:
return 3901
try:
return int(raw)
except (TypeError, ValueError):
return 3901

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 _ui_port() is defined identically in both system.py (here) and main.py. The comment says it "mirrors" main.py to avoid a circular import, but the two copies can silently drift if one is updated without the other. Moving _ui_port into network_share alongside backend_port() would give a single source of truth without the import problem.

Suggested change
def _ui_port() -> int:
"""The Vite UI dev-server port, single-sourced from OMNIVOICE_UI_PORT.
Mirrors the resolver in main.py (kept local to avoid importing the app
module). Falls back to 3901 on a missing or malformed value.
"""
raw = os.environ.get("OMNIVOICE_UI_PORT")
if raw is None:
return 3901
try:
return int(raw)
except (TypeError, ValueError):
return 3901
def _ui_port() -> int:
"""The Vite UI dev-server port, single-sourced from OMNIVOICE_UI_PORT.
Moved to network_share to keep all port helpers together and avoid
duplication with main.py. Falls back to 3901 on a missing or malformed
valuenever throws.
"""
from services import network_share as _ns # local import avoids circular dep
return _ns.ui_port()

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@debpalash
debpalash merged commit c2869e7 into main May 30, 2026
14 of 15 checks passed
@debpalash
debpalash deleted the feat/configurable-ports branch June 12, 2026 10:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant