Skip to content

RTSP installer changes - #13

Closed
Kirusthiya wants to merge 1 commit into
mainfrom
installer_rstp
Closed

RTSP installer changes#13
Kirusthiya wants to merge 1 commit into
mainfrom
installer_rstp

Conversation

@Kirusthiya

@Kirusthiya Kirusthiya commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

change

Summary by CodeRabbit

  • New Features
    • Added camera editing, removal, bulk selection, and disabled-camera management.
    • Added live camera previews, status monitoring, and improved snapshot access.
    • Added monitoring pause/resume controls and clearer connector setup status.
    • Added automatic connector update notifications and installation.
  • Bug Fixes
    • Improved camera source changes, reconnect handling, and local video playback.
    • Prevented duplicate connector pairing and hidden disabled cameras from connector views.
  • Release
    • Updated the Windows Connector installer to version 1.1.12.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d1a213fe-2c23-462a-b773-92bc7af12948

📥 Commits

Reviewing files that changed from the base of the PR and between e36007d and 10bc82c.

📒 Files selected for processing (31)
  • .env.example
  • backend/Contracts/Dtos.cs
  • backend/Controllers/CamerasController.cs
  • backend/Controllers/ConnectorsController.cs
  • backend/Domain/Enums.cs
  • backend/appsettings.json
  • connector/app/admin.py
  • connector/app/backend_client.py
  • connector/app/baked_config.py
  • connector/app/capture.py
  • connector/app/config.py
  • connector/app/main.py
  • connector/app/orchestrator.py
  • connector/app/paths.py
  • connector/app/runtime.py
  • connector/app/store.py
  • connector/app/tray.py
  • connector/app/wizard.py
  • connector/installer/onevo-connector.iss
  • connector/onevo_launcher.py
  • connector/tests/test_installer_config.py
  • connector/tests/test_rtsp.py
  • connector/tests/test_runtime_safety.py
  • dashboard/src/app/core/api.service.ts
  • dashboard/src/app/pages/get-started/get-started.component.ts
  • dashboard/src/app/pages/setup/setup.component.ts
  • docker-compose.yml
  • installer-site/.gitignore
  • installer-site/.vercelignore
  • installer-site/index.html
  • installer-site/latest.json

📝 Walkthrough

Walkthrough

Changes

The PR adds disabled-camera management, live camera administration, persistent pause and resume behavior, connector uninstall handling, source synchronization, installer update support, and connector version 1.1.12 metadata across the backend, connector, installer, and Angular dashboard.

Connector platform

Layer / File(s) Summary
Backend camera lifecycle
backend/Contracts/Dtos.cs, backend/Domain/Enums.cs, backend/Controllers/CamerasController.cs, backend/Controllers/ConnectorsController.cs
Adds disabled camera state, single and bulk disable endpoints, duplicate-connector claim protection, uninstall state updates, and filtering of disabled cameras.
Runtime state and capture pipeline
connector/app/config.py, connector/app/runtime.py, connector/app/capture.py, connector/app/orchestrator.py, connector/app/store.py, connector/app/paths.py
Adds camera status/frame state, processing-width limits, paced capture, preview publishing, pause handling, source fingerprint restarts, local file normalization, and persisted pause settings.
Admin source and live management
connector/app/admin.py
Adds keyed source synchronization and deletion, setup gating, persistent pause handling, live-camera endpoints, MJPEG streaming, and the updated admin dashboard.
Setup, startup, and uninstall wiring
connector/app/main.py, connector/app/wizard.py, connector/app/backend_client.py, connector/onevo_launcher.py
Updates provisioning persistence, event-driven activation retries, paused-state restoration, and uninstall notification routing.
Installer and tray update flow
connector/app/tray.py, connector/installer/onevo-connector.iss, .env.example, installer-site/*, docker-compose.yml, backend/appsettings.json
Adds installer update mode, pause-marker handling, tray update discovery and verification, and version 1.1.12 release metadata.
Dashboard camera and connector management
dashboard/src/app/core/api.service.ts, dashboard/src/app/pages/get-started/get-started.component.ts, dashboard/src/app/pages/setup/setup.component.ts
Adds installed-versus-online connector messaging, update detection, masked camera details, inline editing, selection, and single/bulk camera removal.
Validation and release support
connector/tests/*
Updates installer persistence patches and adds coverage for local MP4 paths, persisted pause settings, and tray update states.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CapturePipeline
  participant RuntimeState
  participant AdminDashboard
  CapturePipeline->>RuntimeState: Publish camera frame and status
  AdminDashboard->>RuntimeState: Request live camera status or frame
  RuntimeState-->>AdminDashboard: Return status or MJPEG frame
Loading
sequenceDiagram
  participant TrayApplication
  participant UpdateManifest
  participant Installer
  TrayApplication->>UpdateManifest: Fetch latest release metadata
  UpdateManifest-->>TrayApplication: Return version, URL, size, and SHA-256
  TrayApplication->>TrayApplication: Verify downloaded installer
  TrayApplication->>Installer: Launch update mode
Loading

Possibly related PRs

Suggested reviewers: baasith6

✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch installer_rstp
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch installer_rstp

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.

@Kirusthiya Kirusthiya closed this Jul 29, 2026
@Kirusthiya
Kirusthiya deleted the installer_rstp branch July 29, 2026 09:55
@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. All-sources delete can't sync 🐞 Bug ≡ Correctness
Description
The new local admin bulk-delete flow can remove every source, but then calls finalize_setup with an
empty source-key list; the backend FinalizeSetup rejects empty lists, so the backend source set
cannot be cleared and local vs backend state diverges.
Code

connector/app/admin.py[R130-143]

+    def _sync_sources(wizard: WizardConfig) -> bool:
+        connector_id = store.get_cred("connector_id") if store else None
+        api_key = store.get_cred("api_key") if store else None
+        if client is None or not (connector_id and api_key):
+            wizard.setup_complete = False
+            save_wizard_config(wizard)
+            return False
+        client.set_credentials(connector_id, api_key)
+        client.finalize_setup([
+            source.source_key or source_key_for(source) for source in wizard.sources
+        ])
+        wizard.setup_complete = True
+        save_wizard_config(wizard)
+        return True
Relevance

⭐⭐ Medium

PR #7 shows backend FinalizeSetup rejects empty SourceKeys; no prior accepted fix allowing empty
authoritative set.

PR-#7

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR’s bulk-delete feature can leave wizard.sources empty and then calls finalize_setup with
the remaining keys; the backend FinalizeSetup implementation rejects empty SourceKeys, making
“delete all sources” impossible to finalize.

connector/app/admin.py[130-143]
connector/app/admin.py[191-212]
backend/Controllers/ConnectorsController.cs[254-294]
PR-#4

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The connector local admin UI can delete all configured sources, but `_sync_sources()` always calls `client.finalize_setup([...])` and will send an empty list after deleting the last source. The backend `FinalizeSetup` currently rejects empty `SourceKeys`, so “remove all sources” cannot be finalized and backend cameras remain attached.

### Issue Context
- This PR introduces bulk-delete in the connector admin UI and source-set syncing via `finalize_setup`.
- Backend `FinalizeSetup` currently enforces `SourceKeys.Count > 0`.

### Fix Focus Areas
- backend/Controllers/ConnectorsController.cs[254-294]
- connector/app/admin.py[130-143]
- connector/app/admin.py[191-212]

### Suggested fix
1. **Backend:** Update `FinalizeSetup` to accept an empty `SourceKeys` list as a valid “detach everything for this connector” operation (execute the detach update for all cameras with `ConnectorId == connector.Id`). Return `Ok` with `activeCameraCount = 0`.
2. **Connector admin:** In `_sync_sources`, if `wizard.sources` is empty, still call `finalize_setup([])` (after backend supports it) and then set `wizard.setup_complete = True`.
3. Add/adjust a test to cover deleting the last source and verifying backend detachment.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Updater path traversal 🐞 Bug ⛨ Security
Description
TrayApplication builds the downloaded update filename from latest_version fetched from a remote
JSON manifest without sanitizing it, allowing path separators/".." in the version to escape the
updates directory and write to unintended paths.
Code

connector/app/tray.py[R295-351]

+    def _check_updates(self, notify: bool = True) -> None:
+        try:
+            response = requests.get(UPDATE_MANIFEST_URL, timeout=15)
+            response.raise_for_status()
+            metadata = response.json()
+            latest = str(metadata.get("version") or "").strip()
+            available = bool(latest) and (
+                self._version_tuple(latest) > self._version_tuple(CURRENT_VERSION)
+            )
+            newly_available = available and latest != self.latest_version
+            self.update_metadata = metadata if available else {}
+            self.latest_version = latest if available else ""
+            self.update_available = available
+            self._last_update_check = time.monotonic()
+            if newly_available and notify and self.icon is not None:
+                self._prompt_for_update(latest)
+        except (requests.RequestException, ValueError, TypeError):
+            self._last_update_check = time.monotonic()
+        if self.icon is not None:
+            self.icon.update_menu()
+
+    def _install_update(self, _icon=None, _item=None) -> None:
+        if self.update_busy or not self.update_available:
+            return
+
+        def worker():
+            self.update_busy = True
+            if self.icon is not None:
+                self.icon.update_menu()
+            try:
+                metadata = self.update_metadata
+                download_url = str(metadata.get("downloadUrl") or "").strip()
+                expected_hash = str(metadata.get("sha256") or "").strip().lower()
+                expected_size = int(metadata.get("sizeBytes") or 0)
+                if not download_url.lower().startswith("https://"):
+                    raise RuntimeError("The update download URL is not secure.")
+                update_dir = self.state_dir.parent / "updates"
+                update_dir.mkdir(parents=True, exist_ok=True)
+                target = update_dir / f"ONEVO-Connector-Update-{self.latest_version}.exe"
+                partial = target.with_suffix(".exe.partial")
+                digest = hashlib.sha256()
+                received = 0
+                self._notify(f"Downloading ONEVO Connector {self.latest_version}...")
+                with requests.get(download_url, stream=True, timeout=(15, 120)) as response:
+                    response.raise_for_status()
+                    with partial.open("wb") as output:
+                        for chunk in response.iter_content(1024 * 1024):
+                            if not chunk:
+                                continue
+                            output.write(chunk)
+                            digest.update(chunk)
+                            received += len(chunk)
+                if expected_size and received != expected_size:
+                    raise RuntimeError("Downloaded update size does not match the release.")
+                if not expected_hash or digest.hexdigest().lower() != expected_hash:
+                    raise RuntimeError("Downloaded update failed SHA-256 verification.")
+                partial.replace(target)
Relevance

⭐⭐ Medium

No prior accepted/rejected findings found about sanitizing remote manifest version used in local
update filenames (path traversal).

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The code reads version from a remote manifest and stores it as latest_version, then uses it
directly in a Path join to form the local output path that is opened for writing.

connector/app/tray.py[295-351]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`latest_version` is taken from the remote update manifest and interpolated directly into the local update filename. If the manifest contains `..`, `\\`, or `/`, the resulting `Path` can escape the intended `updates` directory, enabling arbitrary file write locations before any hash verification.

### Issue Context
- `latest_version` is set from `UPDATE_MANIFEST_URL` JSON.
- The code writes to `partial.open("wb")` using a path derived from that version.

### Fix Focus Areas
- connector/app/tray.py[295-351]

### Suggested fix
1. Validate the manifest version strictly (e.g., `re.fullmatch(r"\d+(?:\.\d+){0,3}", latest)`), and **reject** versions containing any path separators or dot-dot sequences.
2. Build the target filename from the **sanitized** version only, or avoid embedding it at all (e.g., always download to a fixed filename like `ONEVO-Connector-Update.exe` and keep the version only in metadata).
3. Add a containment check before writing:
  - Resolve both paths and assert `target.resolve()` is under `update_dir.resolve()` (platform-appropriate).
4. Add a unit test with a malicious version like `"..\\..\\evil"` ensuring the update is rejected.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. MJPEG stream cleanup missing 🐞 Bug ☼ Reliability
Description
The new MJPEG streaming endpoint uses a synchronous infinite generator with time.sleep and no
explicit disconnect/cancellation handling, relying entirely on framework behavior for cleanup and
potentially increasing worker usage under frequent connect/disconnect.
Code

connector/app/admin.py[R603-621]

+    @app.get("/live/cameras/{camera_id}/stream.mjpg")
+    def live_camera_stream(camera_id: str):
+        def frames():
+            last_frame = None
+            while True:
+                frame = state.get_frame(camera_id)
+                if frame and frame is not last_frame:
+                    last_frame = frame
+                    yield (
+                        b"--frame\r\nContent-Type: image/jpeg\r\n"
+                        + f"Content-Length: {len(frame)}\r\n\r\n".encode("ascii")
+                        + frame
+                        + b"\r\n"
+                    )
+                time.sleep(0.125)
+
+        return StreamingResponse(
+            frames(), media_type="multipart/x-mixed-replace; boundary=frame"
+        )
Relevance

⭐⭐ Medium

No historical review suggestions found about FastAPI MJPEG StreamingResponse disconnect/cancellation
handling in this repo.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR introduces a while True generator with no break condition or disconnect checks, so stream
lifecycle depends on implicit server cancellation semantics.

connector/app/admin.py[603-621]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`/live/cameras/{camera_id}/stream.mjpg` streams via an infinite sync generator and does not explicitly handle client disconnects or cancellation. While Starlette/Uvicorn may close/cancel iterators, adding explicit handling makes behavior deterministic and more robust.

### Issue Context
- Endpoint is synchronous (`def`) and uses `time.sleep(0.125)` in a `while True` loop.

### Fix Focus Areas
- connector/app/admin.py[603-621]

### Suggested fix
1. Accept a `Request` object and switch to an **async generator**:
  - `async def frames(): ...; if await request.is_disconnected(): break; await asyncio.sleep(...)`
2. Wrap the generator loop with `try/finally` to ensure any per-stream cleanup is executed.
3. Optionally add a max stream duration / idle timeout to avoid forgotten tabs keeping streams open indefinitely.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Connector 1.1.12: installer/tray updates, live preview, and camera disable flows

✨ Enhancement 🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Bump Windows connector/installer release metadata to v1.1.12 and update distribution
 URLs/checksums.
• Add camera disable (single + bulk) and ensure disabled cameras are excluded from connector
 capture.
• Improve connector operations: persistent pause/stop behavior, live MJPEG preview, source
 management, and tray auto-update.
Diagram

graph TD
  dash["Dashboard UI"] -->|"REST"| api["Backend API"] -->|"EF Core"| db[("Postgres")]
  conn["Connector service"] -->|"heartbeat / cameras"| api
  admin["Local admin UI"] -->|"control / preview"| conn
  admin -->|"provision / finalize"| api
  tray["Tray + installer"] -->|"start/stop + pause"| conn
  tray -->|"update download"| rel{{"Installer site"}}
  api -->|"installer metadata"| rel

  subgraph Legend
    direction LR
    _comp["Component"] ~~~ _db[("Database")] ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Soft-delete cameras with DisabledAt + reason fields
  • ➕ Keeps disable timestamp/reason without overloading enum semantics
  • ➕ Easier future re-enable/reporting without status proliferation
  • ➖ Requires schema/migration work and broader query updates
  • ➖ More code churn than adding a single enum value
2. Signed update manifest + Authenticode verification
  • ➕ Stronger supply-chain protection than size/SHA-256 alone
  • ➕ Reduces risk if manifest hosting is compromised
  • ➖ Requires certificate management and CI signing pipeline
  • ➖ More implementation complexity for the current stage
3. WebSocket/WebRTC live preview instead of MJPEG
  • ➕ Lower latency and better bandwidth efficiency for multi-camera preview
  • ➕ More extensible for richer live diagnostics
  • ➖ Significantly more complex than MJPEG multipart streaming
  • ➖ Harder to debug/deploy compared to a simple HTTP stream

Recommendation: The PR’s approach is a good near-term tradeoff: Disabled status is simple to implement and reason about, and MJPEG streaming is the lowest-complexity live preview that works everywhere. If auto-updates become widely deployed, the most valuable follow-up would be to harden trust (signed manifest / installer verification) beyond checksum + HTTPS.

Files changed (31) +1315 / -151

Enhancement (16) +1075 / -92
Dtos.csAdd bulk-disable cameras request DTO +1/-0

Add bulk-disable cameras request DTO

• Introduces BulkDisableCamerasRequest carrying a list of camera IDs for batch disable operations.

backend/Contracts/Dtos.cs

CamerasController.csAdd single and bulk camera disable endpoints +26/-0

Add single and bulk camera disable endpoints

• Adds DELETE /api/cameras/{id} to mark a camera Disabled with tenant access checks. Adds POST /api/cameras/bulk-disable to disable multiple cameras using a tenant-scoped query.

backend/Controllers/CamerasController.cs

Enums.csIntroduce Disabled camera status +2/-1

Introduce Disabled camera status

• Extends CameraStatus with Disabled to represent cameras removed from monitoring without deleting historical records.

backend/Domain/Enums.cs

admin.pyAdd source management, live MJPEG preview, and persistent pause behavior +389/-37

Add source management, live MJPEG preview, and persistent pause behavior

• Adds masked display of RTSP credentials, stable source keys, update-by-key and bulk delete endpoints, and management gating until connector pairing is complete. Adds live camera status endpoints and MJPEG streaming, persists pause state, and on Windows pauses by stopping the service after responding; also binds admin host to localhost by default.

connector/app/admin.py

backend_client.pyAdd backend uninstall notification call +8/-0

Add backend uninstall notification call

• Adds notify_uninstall() to POST /api/connectors/uninstall using connector auth headers.

connector/app/backend_client.py

capture.pyPublish per-camera runtime status, previews, pause-resume reopen, and resizing +100/-12

Publish per-camera runtime status, previews, pause-resume reopen, and resizing

• Publishes per-camera runtime state (Live/Reconnecting/Paused/Offline) and JPEG previews via RuntimeState. Implements pause that releases capture resources and reopens on resume, adds bounded processing frame width, and throttles file-source reads to match source FPS for stable playback.

connector/app/capture.py

config.pyBump connector version and add processing_max_width configuration +3/-1

Bump connector version and add processing_max_width configuration

• Updates runtime version to 1.1.12 and adds CONNECTOR_PROCESSING_MAX_WIDTH (min 320) to cap processing resolution.

connector/app/config.py

paths.pyPrefer CONNECTOR_PROGRAM_DATA override and add pause marker path +8/-1

Prefer CONNECTOR_PROGRAM_DATA override and add pause marker path

• Ensures CONNECTOR_PROGRAM_DATA overrides Windows PROGRAMDATA to keep tests/portable runs from touching global state. Adds pause_marker_path() used to persist monitoring-paused state machine-wide.

connector/app/paths.py

runtime.pyAdd thread-safe camera runtime state tracking +46/-0

Add thread-safe camera runtime state tracking

• Introduces camera_states plus helpers to publish frames with metadata, set statuses/errors, remove cameras, list camera statuses, and safely fetch last frames. Pausing clears last frames and marks cameras Paused.

connector/app/runtime.py

store.pyAdd persistent boolean settings helpers +9/-0

Add persistent boolean settings helpers

• Adds get_bool_setting/set_bool_setting backed by existing credential storage, used for persisting monitoring_paused across restarts.

connector/app/store.py

tray.pyImplement tray auto-update and elevated resume monitoring +233/-2

Implement tray auto-update and elevated resume monitoring

• Adds update checks via latest.json manifest, prompts once per version, verifies download size and SHA-256, and runs installer elevated for updates. Adds persistent pause marker behavior (block start while paused), an elevated “Start monitoring” action, and service autostart toggling.

connector/app/tray.py

onevo-connector.issMake updates safer and respect persistent pause marker +59/-20

Make updates safer and respect persistent pause marker

• Bumps AppVersion to 1.1.12; avoids uninstall/reinstall service races during in-place updates, introduces /UPDATE mode to skip config pages, and enforces non-empty selections when a source type is chosen. Adds pause-marker-aware service start behavior and an uninstall-time cloud notification step.

connector/installer/onevo-connector.iss

onevo_launcher.pyAdd uninstall notify and start-monitoring commands +29/-0

Add uninstall notify and start-monitoring commands

• Adds --notify-uninstall (best-effort backend status update before local data removal) and --start-monitoring (resume monitoring helper used by tray elevation).

connector/onevo_launcher.py

api.service.tsAdd camera delete and bulk-disable API methods +7/-0

Add camera delete and bulk-disable API methods

• Introduces deleteCamera() and bulkDisableCameras() to call the new backend camera disable endpoints.

dashboard/src/app/core/api.service.ts

get-started.component.tsClarify connector onboarding step for offline/uninstalled connectors +16/-5

Clarify connector onboarding step for offline/uninstalled connectors

• Tracks installed connector count separately from online connectors and adjusts the onboarding step title/detail and download action depending on connector health/state.

dashboard/src/app/pages/get-started/get-started.component.ts

setup.component.tsAdd camera edit + bulk remove UX; mask RTSP; show update hints +139/-13

Add camera edit + bulk remove UX; mask RTSP; show update hints

• Adds checkbox selection with select-all and bulk remove (disable), plus per-camera edit and remove actions. Masks RTSP passwords, updates snapshot link to camera-id query endpoint, filters out Disabled cameras, and surfaces connector update-available messaging when installed.

dashboard/src/app/pages/setup/setup.component.ts

Bug fix (3) +122 / -15
ConnectorsController.csBlock duplicate active connector claims and add uninstall endpoint +38/-1

Block duplicate active connector claims and add uninstall endpoint

• Prevents setup-code claim if the store already has a recently-heartbeating connector (returns 409). Adds /api/connectors/uninstall to mark the connector Offline and clear heartbeat/queue state, and filters Disabled cameras out of the connector camera list.

backend/Controllers/ConnectorsController.cs

main.pyPersist paused state and improve installer provisioning retry logic +52/-6

Persist paused state and improve installer provisioning retry logic

• Restores paused state from LocalStore on startup and passes BackendClient to admin for management readiness. Updates native installer provisioning to handle pending setups using saved credentials when no setup code is present, with clearer wait/retry behavior.

connector/app/main.py

orchestrator.pyRestart pipelines when source changes and normalize file:// sources +32/-8

Restart pipelines when source changes and normalize file:// sources

• Tracks source fingerprints per camera and recreates pipelines when RTSP/ONVIF/status changes. Converts file:// URIs to native paths on Windows for OpenCV and centralizes pipeline teardown/cleanup.

connector/app/orchestrator.py

Refactor (1) +0 / -24
wizard.pyRemove redundant wizard-side camera creation logic +0/-24

Remove redundant wizard-side camera creation logic

• Eliminates duplicated camera provisioning code and avoids persisting connector credentials directly in wizard_claim, relying on shared provisioning flows instead.

connector/app/wizard.py

Tests (3) +100 / -4
test_installer_config.pyFix patch targets for installer provisioning tests +13/-3

Fix patch targets for installer provisioning tests

• Updates mocks to patch the correct save_wizard_config call sites after refactoring provisioning/save paths.

connector/tests/test_installer_config.py

test_rtsp.pyTest file:// MP4 URI conversion to native path in orchestrator +33/-0

Test file:// MP4 URI conversion to native path in orchestrator

• Extends minimal config with processing_max_width and adds a test ensuring file:// URIs become native Windows paths and loop mode is enabled for local videos.

connector/tests/test_rtsp.py

test_runtime_safety.pyAdd tests for pause persistence and tray update state +54/-1

Add tests for pause persistence and tray update state

• Adds coverage for LocalStore boolean settings surviving reopen and for tray update availability/prompt tracking based on manifest versions.

connector/tests/test_runtime_safety.py

Other (8) +18 / -16
.env.exampleBump published connector installer metadata to v1.1.12 +4/-4

Bump published connector installer metadata to v1.1.12

• Updates the example environment values for connector installer version, hosted EXE URL, file size (bytes), and SHA-256 checksum to match the 1.1.12 release.

.env.example

appsettings.jsonUpdate backend connector installer version to 1.1.12 +1/-1

Update backend connector installer version to 1.1.12

• Bumps ConnectorInstaller:Version to keep backend metadata aligned with the new release.

backend/appsettings.json

baked_config.pyAdjust baked backend URL +1/-1

Adjust baked backend URL

• Changes baked backend URL to http://localhost:8081 for the generated connector build config.

connector/app/baked_config.py

docker-compose.ymlDefault connector installer version to 1.1.12 +1/-1

Default connector installer version to 1.1.12

• Updates docker-compose default ConnectorInstaller__Version to 1.1.12 for local/dev parity.

docker-compose.yml

.gitignoreIgnore installer-site environment files +1/-0

Ignore installer-site environment files

• Adds .env* to prevent committing installer-site environment config/secrets.

installer-site/.gitignore

.vercelignoreExclude EXE binaries from Vercel deploy output +1/-0

Exclude EXE binaries from Vercel deploy output

• Ignores *.exe so release binaries are hosted separately rather than deployed with the static site.

installer-site/.vercelignore

index.htmlUpdate release landing page to v1.1.12 and external EXE URL +4/-4

Update release landing page to v1.1.12 and external EXE URL

• Updates displayed version/file/size and points the download button directly at the hosted 1.1.12 installer EXE.

installer-site/index.html

latest.jsonPublish v1.1.12 update manifest +5/-5

Publish v1.1.12 update manifest

• Updates latest.json to 1.1.12 with downloadUrl, sizeBytes, and sha256 used by the tray updater for verification.

installer-site/latest.json

Comment thread connector/app/admin.py
Comment on lines +130 to +143
def _sync_sources(wizard: WizardConfig) -> bool:
connector_id = store.get_cred("connector_id") if store else None
api_key = store.get_cred("api_key") if store else None
if client is None or not (connector_id and api_key):
wizard.setup_complete = False
save_wizard_config(wizard)
return False
client.set_credentials(connector_id, api_key)
client.finalize_setup([
source.source_key or source_key_for(source) for source in wizard.sources
])
wizard.setup_complete = True
save_wizard_config(wizard)
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. All-sources delete can't sync 🐞 Bug ≡ Correctness

The new local admin bulk-delete flow can remove every source, but then calls finalize_setup with an
empty source-key list; the backend FinalizeSetup rejects empty lists, so the backend source set
cannot be cleared and local vs backend state diverges.
Agent Prompt
### Issue description
The connector local admin UI can delete all configured sources, but `_sync_sources()` always calls `client.finalize_setup([...])` and will send an empty list after deleting the last source. The backend `FinalizeSetup` currently rejects empty `SourceKeys`, so “remove all sources” cannot be finalized and backend cameras remain attached.

### Issue Context
- This PR introduces bulk-delete in the connector admin UI and source-set syncing via `finalize_setup`.
- Backend `FinalizeSetup` currently enforces `SourceKeys.Count > 0`.

### Fix Focus Areas
- backend/Controllers/ConnectorsController.cs[254-294]
- connector/app/admin.py[130-143]
- connector/app/admin.py[191-212]

### Suggested fix
1. **Backend:** Update `FinalizeSetup` to accept an empty `SourceKeys` list as a valid “detach everything for this connector” operation (execute the detach update for all cameras with `ConnectorId == connector.Id`). Return `Ok` with `activeCameraCount = 0`.
2. **Connector admin:** In `_sync_sources`, if `wizard.sources` is empty, still call `finalize_setup([])` (after backend supports it) and then set `wizard.setup_complete = True`.
3. Add/adjust a test to cover deleting the last source and verifying backend detachment.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread connector/app/tray.py
Comment on lines +295 to +351
def _check_updates(self, notify: bool = True) -> None:
try:
response = requests.get(UPDATE_MANIFEST_URL, timeout=15)
response.raise_for_status()
metadata = response.json()
latest = str(metadata.get("version") or "").strip()
available = bool(latest) and (
self._version_tuple(latest) > self._version_tuple(CURRENT_VERSION)
)
newly_available = available and latest != self.latest_version
self.update_metadata = metadata if available else {}
self.latest_version = latest if available else ""
self.update_available = available
self._last_update_check = time.monotonic()
if newly_available and notify and self.icon is not None:
self._prompt_for_update(latest)
except (requests.RequestException, ValueError, TypeError):
self._last_update_check = time.monotonic()
if self.icon is not None:
self.icon.update_menu()

def _install_update(self, _icon=None, _item=None) -> None:
if self.update_busy or not self.update_available:
return

def worker():
self.update_busy = True
if self.icon is not None:
self.icon.update_menu()
try:
metadata = self.update_metadata
download_url = str(metadata.get("downloadUrl") or "").strip()
expected_hash = str(metadata.get("sha256") or "").strip().lower()
expected_size = int(metadata.get("sizeBytes") or 0)
if not download_url.lower().startswith("https://"):
raise RuntimeError("The update download URL is not secure.")
update_dir = self.state_dir.parent / "updates"
update_dir.mkdir(parents=True, exist_ok=True)
target = update_dir / f"ONEVO-Connector-Update-{self.latest_version}.exe"
partial = target.with_suffix(".exe.partial")
digest = hashlib.sha256()
received = 0
self._notify(f"Downloading ONEVO Connector {self.latest_version}...")
with requests.get(download_url, stream=True, timeout=(15, 120)) as response:
response.raise_for_status()
with partial.open("wb") as output:
for chunk in response.iter_content(1024 * 1024):
if not chunk:
continue
output.write(chunk)
digest.update(chunk)
received += len(chunk)
if expected_size and received != expected_size:
raise RuntimeError("Downloaded update size does not match the release.")
if not expected_hash or digest.hexdigest().lower() != expected_hash:
raise RuntimeError("Downloaded update failed SHA-256 verification.")
partial.replace(target)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Updater path traversal 🐞 Bug ⛨ Security

TrayApplication builds the downloaded update filename from latest_version fetched from a remote
JSON manifest without sanitizing it, allowing path separators/".." in the version to escape the
updates directory and write to unintended paths.
Agent Prompt
### Issue description
`latest_version` is taken from the remote update manifest and interpolated directly into the local update filename. If the manifest contains `..`, `\\`, or `/`, the resulting `Path` can escape the intended `updates` directory, enabling arbitrary file write locations before any hash verification.

### Issue Context
- `latest_version` is set from `UPDATE_MANIFEST_URL` JSON.
- The code writes to `partial.open("wb")` using a path derived from that version.

### Fix Focus Areas
- connector/app/tray.py[295-351]

### Suggested fix
1. Validate the manifest version strictly (e.g., `re.fullmatch(r"\d+(?:\.\d+){0,3}", latest)`), and **reject** versions containing any path separators or dot-dot sequences.
2. Build the target filename from the **sanitized** version only, or avoid embedding it at all (e.g., always download to a fixed filename like `ONEVO-Connector-Update.exe` and keep the version only in metadata).
3. Add a containment check before writing:
   - Resolve both paths and assert `target.resolve()` is under `update_dir.resolve()` (platform-appropriate).
4. Add a unit test with a malicious version like `"..\\..\\evil"` ensuring the update is rejected.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread connector/app/admin.py
Comment on lines +603 to +621
@app.get("/live/cameras/{camera_id}/stream.mjpg")
def live_camera_stream(camera_id: str):
def frames():
last_frame = None
while True:
frame = state.get_frame(camera_id)
if frame and frame is not last_frame:
last_frame = frame
yield (
b"--frame\r\nContent-Type: image/jpeg\r\n"
+ f"Content-Length: {len(frame)}\r\n\r\n".encode("ascii")
+ frame
+ b"\r\n"
)
time.sleep(0.125)

return StreamingResponse(
frames(), media_type="multipart/x-mixed-replace; boundary=frame"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

3. Mjpeg stream cleanup missing 🐞 Bug ☼ Reliability

The new MJPEG streaming endpoint uses a synchronous infinite generator with time.sleep and no
explicit disconnect/cancellation handling, relying entirely on framework behavior for cleanup and
potentially increasing worker usage under frequent connect/disconnect.
Agent Prompt
### Issue description
`/live/cameras/{camera_id}/stream.mjpg` streams via an infinite sync generator and does not explicitly handle client disconnects or cancellation. While Starlette/Uvicorn may close/cancel iterators, adding explicit handling makes behavior deterministic and more robust.

### Issue Context
- Endpoint is synchronous (`def`) and uses `time.sleep(0.125)` in a `while True` loop.

### Fix Focus Areas
- connector/app/admin.py[603-621]

### Suggested fix
1. Accept a `Request` object and switch to an **async generator**:
   - `async def frames(): ...; if await request.is_disconnected(): break; await asyncio.sleep(...)`
2. Wrap the generator loop with `try/finally` to ensure any per-stream cleanup is executed.
3. Optionally add a max stream duration / idle timeout to avoid forgotten tabs keeping streams open indefinitely.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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