Skip to content

api fixes - #12

Merged
baasith6 merged 2 commits into
mainfrom
azure-mvp-deploy
Jul 29, 2026
Merged

api fixes#12
baasith6 merged 2 commits into
mainfrom
azure-mvp-deploy

Conversation

@baasith6

@baasith6 baasith6 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added Analytics, Reports, System Logs, and Settings pages.
    • Added CSV report export and password changes.
    • Added bulk deletion for alerts and clips.
    • Added connector update checks and configurable live snapshot connections.
  • Bug Fixes
    • Improved alert visibility and tenant access controls.
    • Improved connector heartbeat, setup, and RTSP installer flows.
    • Added automatic session handling when authentication expires.
  • Security
    • Added protected connector administration and stronger production configuration validation.
    • Improved service authentication and secure deployment guidance.

@cursor

cursor Bot commented Jul 29, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@baasith6
baasith6 merged commit e36007d into main Jul 29, 2026
0 of 3 checks passed
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@baasith6, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 57 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 83e2bad0-7380-4ebe-8b32-72673017da31

📥 Commits

Reviewing files that changed from the base of the PR and between 09451e1 and d9843da.

📒 Files selected for processing (47)
  • .cursor/plans/onevo_system_audit_ebd9d4dc.plan.md
  • .env.example
  • backend/Auth/ServiceAuth.cs
  • backend/Contracts/Dtos.cs
  • backend/Controllers/AiEventsController.cs
  • backend/Controllers/AlertsController.cs
  • backend/Controllers/AnalyticsController.cs
  • backend/Controllers/AuthController.cs
  • backend/Controllers/ClipsController.cs
  • backend/Controllers/ConnectorsController.cs
  • backend/Controllers/HealthController.cs
  • backend/Controllers/LogsController.cs
  • backend/Controllers/ServiceController.cs
  • backend/Controllers/ZonesController.cs
  • backend/Data/DbSeeder.cs
  • backend/Domain/Entities.cs
  • backend/Program.cs
  • backend/Services/TheftOrchestrator.cs
  • backend/appsettings.json
  • cloud-ai/app/config.py
  • cloud-ai/app/main.py
  • connector/app/admin.py
  • connector/app/backend_client.py
  • connector/app/config.py
  • connector/app/main.py
  • connector/app/network_util.py
  • connector/app/update_check.py
  • connector/app/wizard.py
  • connector/app/workers.py
  • connector/installer/onevo-connector.iss
  • connector/tests/test_installer_config.py
  • connector/tests/test_rtsp.py
  • dashboard/src/app/app.routes.ts
  • dashboard/src/app/core/api.service.ts
  • dashboard/src/app/core/auth.interceptor.ts
  • dashboard/src/app/core/models.ts
  • dashboard/src/app/pages/alerts/alerts.component.ts
  • dashboard/src/app/pages/analytics/analytics.component.ts
  • dashboard/src/app/pages/clips/clips.component.ts
  • dashboard/src/app/pages/logs/logs.component.ts
  • dashboard/src/app/pages/reports/reports.component.ts
  • dashboard/src/app/pages/settings/settings.component.ts
  • dashboard/src/app/pages/setup/setup.component.ts
  • dashboard/src/app/shell/shell.component.ts
  • docker-compose.yml
  • infra/mvp/.env.production.example
  • infra/mvp/nginx-tls.example.conf
📝 Walkthrough

Walkthrough

The PR adds backend service authentication and management APIs, connector admin telemetry and hardening, dashboard analytics/reporting/settings and bulk actions, production configuration templates, and a comprehensive system audit plan.

Changes

ONEVO platform integration

Layer / File(s) Summary
Backend contracts and security wiring
backend/Auth/*, backend/Controllers/*, backend/Data/*, backend/Domain/*, backend/Program.cs, .env.example, docker-compose.yml
Shared service-key validation, tenant checks, connector admin heartbeat fields, production startup validation, and safer seed behavior are added.
Backend management and observability APIs
backend/Controllers/*, backend/Contracts/Dtos.cs, backend/Services/TheftOrchestrator.cs
Analytics, logs, monitoring, password changes, SSE visibility filtering, and bulk alert/clip deletion endpoints are implemented.
Connector administration and runtime reporting
connector/app/*, connector/installer/*, connector/tests/*
Admin token and binding configuration, heartbeat reporting, wizard persistence, RTSP skip handling, public host discovery, and startup update checks are added.
Dashboard workflows and pages
dashboard/src/app/*
Bulk alert/clip actions, analytics, reports, logs, settings, authentication redirects, dynamic snapshots, and navigation routes are added.
Deployment templates and system audit
.cursor/plans/*, infra/mvp/*
The audit plan documents architecture findings and phased work, while production environment and TLS proxy templates are expanded.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Dashboard
  participant Backend
  participant Database
  User->>Dashboard: Select alerts or clips
  Dashboard->>Backend: POST bulk-delete
  Backend->>Database: Resolve scoped records and delete eligible entities
  Database-->>Backend: Deletion counts
  Backend-->>Dashboard: BulkDeleteResponse
  Dashboard-->>User: Refresh list and show result
Loading

Possibly related PRs

Suggested reviewers: kirusthiya

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch azure-mvp-deploy

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.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Harden API auth/tenant scoping and add analytics/logs + bulk delete UX

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

Grey Divider

AI Description

• Separates connector bootstrap and cloud-ai service keys; blocks default secrets in Production.
• Fixes tenant leaks in Zones CRUD and Alerts SSE stream; adds SSE auto-reconnect.
• Adds bulk-delete APIs for alerts/clips plus dashboard UI for selection + delete-all.
• Secures connector admin UI (localhost by default, optional token) and reports admin host/port.
• Adds new Analytics/Reports/Logs/Settings pages and a connector update-availability check.
Diagram

graph TD
  Dashboard["Angular Dashboard"] -->|"JWT REST"| Backend[".NET Backend API"] -->|"EF queries"| Postgres[("PostgreSQL")]
  CloudAI["Cloud AI Worker"] -->|"X-Service-Key (CloudAiServiceKey)"| Backend -->|"persist events/alerts"| Postgres
  Connector["Edge Connector"] -->|"heartbeat adminHost/adminPort"| Backend -->|"connector record"| Dashboard
  Connector -->|"hosts (token-gated)"| AdminUI["Connector Admin UI"] -->|"snapshots"| Dashboard

  subgraph Legend
    direction LR
    _svc([Service]) ~~~ _db[(Database)]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Adopt Azure Key Vault (or similar) for all service keys/secrets
  • ➕ Centralized rotation/auditing and fewer plaintext secrets in deployment files
  • ➕ Clearer separation of concerns for connector vs cloud-ai credentials
  • ➖ Adds infra/setup complexity beyond current single-VM MVP
  • ➖ Not necessary to close the immediate tenant/service-key gaps
2. Move SSE to SignalR/Redis backplane instead of in-process filtering
  • ➕ Scales cleanly to multiple backend instances
  • ➕ Naturally supports per-store channels and fan-out
  • ➖ Bigger architectural shift than needed for current deployment model
  • ➖ Requires additional infra wiring and operational maturity

Recommendation: The PR’s approach (config-driven dedicated cloud-ai key + in-request tenant filtering + production fail-fast checks) is the best incremental step for the current MVP: it closes concrete security gaps with minimal deployment disruption. Longer-term, a secrets manager and a scalable SSE backplane are worthwhile, but deferrable until horizontal scaling is actually planned.

Files changed (47) +2011 / -253

Enhancement (23) +1187 / -197
ServiceAuth.csCentralize service-key resolution/validation +23/-0

Centralize service-key resolution/validation

• Introduces a shared helper for connector bootstrap key and cloud-ai service key lookup/validation, avoiding duplicated inline comparisons across controllers.

backend/Auth/ServiceAuth.cs

Dtos.csAdd DTOs for bulk delete, analytics/logs, and password change +39/-1

Add DTOs for bulk delete, analytics/logs, and password change

• Extends HeartbeatRequest with AdminHost/AdminPort and adds request/response types for new endpoints (analytics summary, system logs, bulk delete, change password).

backend/Contracts/Dtos.cs

AnalyticsController.csExpose analytics summary API +58/-0

Expose analytics summary API

• Adds a tenant-scoped summary endpoint that aggregates alerts/clips counts and groups alerts by type, optionally filtered by storeId.

backend/Controllers/AnalyticsController.cs

AuthController.csAdd change-password endpoint +19/-0

Add change-password endpoint

• Adds an authenticated password change route that verifies the current password and enforces a minimal length constraint for the new password.

backend/Controllers/AuthController.cs

ClipsController.csAdd bulk clip deletion with confirmed-alert safeguards +75/-3

Add bulk clip deletion with confirmed-alert safeguards

• Refactors delete logic into a shared method and introduces bulk-delete support. Skips deletion for clips linked to confirmed alerts and reports deleted/skipped counts.

backend/Controllers/ClipsController.cs

ConnectorsController.csValidate bootstrap key via ServiceAuth and persist admin endpoint details +6/-2

Validate bootstrap key via ServiceAuth and persist admin endpoint details

• Uses ServiceAuth to validate connector registration bootstrap key and persists AdminHost/AdminPort reported via heartbeat for later dashboard use.

backend/Controllers/ConnectorsController.cs

HealthController.csAdd monitoring endpoint for queue depth and environment +23/-0

Add monitoring endpoint for queue depth and environment

• Introduces an Admin/Manager-only endpoint returning basic monitoring metrics (redis queue depth, failed jobs, environment).

backend/Controllers/HealthController.cs

LogsController.csAdd system logs endpoint for connector/queue status +61/-0

Add system logs endpoint for connector/queue status

• Adds an Admin/Manager API returning connector heartbeat/status information plus Redis queue metrics, with optional store filtering.

backend/Controllers/LogsController.cs

Entities.csPersist connector admin endpoint metadata +3/-0

Persist connector admin endpoint metadata

• Adds AdminHost/AdminPort fields to the Connector entity to store the admin UI endpoint reported by the connector.

backend/Domain/Entities.cs

backend_client.pyInclude admin host/port in heartbeat payload +20/-7

Include admin host/port in heartbeat payload

• Extends heartbeat() to optionally send adminHost/adminPort so the backend can record a reachable admin endpoint for dashboard integrations.

connector/app/backend_client.py

main.pyCheck for connector updates on startup +2/-0

Check for connector updates on startup

• Invokes an update check after starting background workers to surface “new installer available” messages in connector logs.

connector/app/main.py

network_util.pyResolve LAN-reachable admin host for heartbeat reporting +19/-0

Resolve LAN-reachable admin host for heartbeat reporting

• Adds a helper to determine the connector machine’s outward-facing IP (with env override) to report as AdminHost.

connector/app/network_util.py

update_check.pyAdd backend-driven installer update check +20/-0

Add backend-driven installer update check

• Adds a small helper that reads /api/connectors/installer from the backend and logs when a newer version is available.

connector/app/update_check.py

app.routes.tsAdd Analytics/Reports/Logs/Settings routes +17/-0

Add Analytics/Reports/Logs/Settings routes

• Introduces lazy-loaded routes for the new pages within the authenticated app shell.

dashboard/src/app/app.routes.ts

api.service.tsAdd client methods for new backend endpoints +52/-1

Add client methods for new backend endpoints

• Adds methods for bulk deletes, analytics summary, system logs, monitoring, and change-password API calls.

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

models.tsAdd models for analytics/logs and connector admin endpoint +32/-0

Add models for analytics/logs and connector admin endpoint

• Adds interfaces for AnalyticsSummary and SystemLogs responses, and extends Connector with adminHost/adminPort fields.

dashboard/src/app/core/models.ts

alerts.component.tsAdd storeId deep-linking, bulk delete UI, and SSE reconnect +115/-12

Add storeId deep-linking, bulk delete UI, and SSE reconnect

• Reads storeId from query params, adds selectable rows with bulk delete/delete-all-in-store for Managers/Admins, and implements exponential-backoff SSE reconnection.

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

analytics.component.tsAdd Analytics page UI +84/-0

Add Analytics page UI

• New page rendering store-filtered aggregate alert/clip metrics and an alerts-by-type table.

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

clips.component.tsAdd storeId deep-linking and bulk clip delete UI +266/-171

Add storeId deep-linking and bulk clip delete UI

• Adds selection + bulk delete/delete-all-in-store flows for Managers/Admins and supports storeId query param deep-linking.

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

logs.component.tsAdd System Logs page UI +92/-0

Add System Logs page UI

• New page showing Redis queue metrics and per-connector status/heartbeat information, with optional store filtering.

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

reports.component.tsAdd Reports page with CSV export +96/-0

Add Reports page with CSV export

• New page listing alerts (optionally by store) and exporting a CSV for audit/compliance workflows.

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

settings.component.tsAdd account settings page for password change +60/-0

Add account settings page for password change

• New page providing a simple change-password form backed by the new backend endpoint.

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

shell.component.tsAdd navigation links for new pages +5/-0

Add navigation links for new pages

• Adds sidebar links for Analytics, Reports, Logs, and a Settings link in the user section.

dashboard/src/app/shell/shell.component.ts

Bug fix (12) +206 / -45
AiEventsController.csAuthenticate AI ingest with dedicated cloud-ai service key +2/-1

Authenticate AI ingest with dedicated cloud-ai service key

• Switches AI ingest auth from the connector bootstrap key to ServiceAuth.CloudAiServiceKey, tightening trust boundaries between services.

backend/Controllers/AiEventsController.cs

AlertsController.csTenant-filter SSE and add bulk alert deletion +59/-0

Tenant-filter SSE and add bulk alert deletion

• Filters SSE events by store access and alert visibility mode to prevent cross-store leaks. Adds an Admin/Manager bulk-delete endpoint with store-scoped or ID-based targeting.

backend/Controllers/AlertsController.cs

ZonesController.csEnforce tenant access for zone operations +17/-3

Enforce tenant access for zone operations

• Adds store authorization checks for listing/creating/updating/deleting zones based on the camera’s StoreId, preventing cross-tenant zone access.

backend/Controllers/ZonesController.cs

DbSeeder.csPrefer EF migrations; gate seed data in Production +23/-5

Prefer EF migrations; gate seed data in Production

• Runs Database.MigrateAsync with an EnsureCreated fallback for legacy/dev databases. Adds schema patch columns for connector AdminHost/AdminPort and disables default admin/demo seed in Production unless explicitly enabled.

backend/Data/DbSeeder.cs

Program.csFail fast on default/weak secrets in Production +19/-0

Fail fast on default/weak secrets in Production

• Adds startup validation to prevent running with the default JWT signing key, default connector bootstrap key, or non-dedicated cloud-ai key in non-development environments.

backend/Program.cs

TheftOrchestrator.csFix cross-camera alert clip URL placeholder +1/-1

Fix cross-camera alert clip URL placeholder

• Replaces the placeholder "multi-camera-event" clip URL with the actual clip ID string so alert detail can resolve playable evidence.

backend/Services/TheftOrchestrator.cs

admin.pySecure admin UI and fix backend client wiring for source management +24/-1

Secure admin UI and fix backend client wiring for source management

• Adds optional token auth middleware (X-Admin-Token or query param) and binds admin server to configurable host (localhost by default). Fixes 503s by instantiating/passing BackendClient with stored credentials when available.

connector/app/admin.py

wizard.pyFix wizard claim crash and duplicate camera provisioning +1/-23

Fix wizard claim crash and duplicate camera provisioning

• Removes undefined-variable credential writes after claim and deletes an accidental second camera-creation loop, preventing duplicates and wizard failures.

connector/app/wizard.py

workers.pyPreserve activation degraded reason and report admin host/port +24/-6

Preserve activation degraded reason and report admin host/port

• Avoids overwriting setup/activation degraded reason in heartbeat, appending operational issues instead. Reports resolved admin host + configured port in heartbeat calls.

connector/app/workers.py

onevo-connector.issAllow skipping RTSP setup without failing validation +3/-1

Allow skipping RTSP setup without failing validation

• Stops RTSP URL validation when the installer flow is explicitly skipping camera setup, unblocking documented “skip” installs.

connector/installer/onevo-connector.iss

auth.interceptor.tsHandle 401 by clearing token and redirecting to login +14/-1

Handle 401 by clearing token and redirecting to login

• Adds error handling to clear auth state and navigate to /login on unauthorized API responses (excluding login itself).

dashboard/src/app/core/auth.interceptor.ts

setup.component.tsResolve connector admin host/port dynamically for snapshots +19/-3

Resolve connector admin host/port dynamically for snapshots

• Replaces hardcoded localhost connector admin host with connector-reported adminHost/adminPort from the backend, fixing remote dashboard snapshot access.

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

Refactor (2) +4 / -7
ServiceController.csReuse ServiceAuth for service-key checks +3/-6

Reuse ServiceAuth for service-key checks

• Replaces custom service-key comparison with ServiceAuth.ValidateCloudAiKey for consistency and configurability.

backend/Controllers/ServiceController.cs

main.pyImport ordering cleanup +1/-1

Import ordering cleanup

• Reorders imports (no functional behavior change).

cloud-ai/app/main.py

Tests (2) +11 / -2
test_installer_config.pyUpdate wizard tests for new store credential access +9/-2

Update wizard tests for new store credential access

• Adds get_cred support to store mocks used by wizard provisioning tests.

connector/tests/test_installer_config.py

test_rtsp.pyUpdate test config for new admin fields +2/-0

Update test config for new admin fields

• Extends the minimal Config fixture to include admin_token and admin_bind_host fields.

connector/tests/test_rtsp.py

Documentation (1) +508 / -0
onevo_system_audit_ebd9d4dc.plan.mdAdd end-to-end ONEVO system audit and roadmap plan +508/-0

Add end-to-end ONEVO system audit and roadmap plan

• Adds a comprehensive audit document detailing architecture, known bugs, security gaps, and a prioritized delivery roadmap. Serves as the reference for the fixes and features implemented across this PR.

.cursor/plans/onevo_system_audit_ebd9d4dc.plan.md

Other (7) +95 / -2
.env.exampleDocument cloud-ai key and connector admin security env vars +6/-0

Document cloud-ai key and connector admin security env vars

• Adds CLOUD_AI_SERVICE_KEY plus connector admin binding and token configuration examples. Clarifies defaults and intended production usage.

.env.example

appsettings.jsonAdd CloudAi:ServiceKey configuration block +3/-0

Add CloudAi:ServiceKey configuration block

• Introduces a CloudAi section for a dedicated service key (dev default).

backend/appsettings.json

config.pySupport dedicated cloud-ai service key env var +4/-1

Support dedicated cloud-ai service key env var

• Reads CLOUD_AI_SERVICE_KEY first and falls back to CONNECTOR_BOOTSTRAP_KEY for backward compatibility.

cloud-ai/app/config.py

config.pyAdd admin token and bind-host configuration +8/-0

Add admin token and bind-host configuration

• Adds admin_token/admin_bind_host fields and loads them from CONNECTOR_ADMIN_TOKEN and CONNECTOR_ADMIN_BIND_LAN/HOST environment variables.

connector/app/config.py

docker-compose.ymlWire CloudAi service key through compose +2/-1

Wire CloudAi service key through compose

• Passes CLOUD_AI_SERVICE_KEY into backend and cloud-ai (with fallback to CONNECTOR_BOOTSTRAP_KEY) to keep dev setups working while enabling dedicated production keys.

docker-compose.yml

.env.production.exampleAdd Cloud AI service key placeholder for production env +1/-0

Add Cloud AI service key placeholder for production env

• Adds CLOUD_AI_SERVICE_KEY placeholder alongside bootstrap key examples for production deployment.

infra/mvp/.env.production.example

nginx-tls.example.confAdd nginx TLS reverse-proxy template +71/-0

Add nginx TLS reverse-proxy template

• Provides an example nginx+certbot configuration for terminating TLS for the dashboard, API, and proxying MinIO uploads over HTTPS.

infra/mvp/nginx-tls.example.conf

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Cloud-AI key not dedicated 🐞 Bug ⛨ Security
Description
Production startup validation does not enforce CloudAi:ServiceKey being different from the connector
bootstrap key, and ServiceAuth.CloudAiServiceKey falls back to the connector key when unset. This
defeats the intended trust-boundary separation and allows the same credential to authorize both
cloud-ai service calls and connector registration.
Code

backend/Program.cs[R123-140]

+// ---- Production secret validation ----
+const string DefaultJwtKey = "dev-super-secret-signing-key-change-me-please-32+";
+if (!builder.Environment.IsDevelopment())
+{
+    if (jwtOpts.SigningKey == DefaultJwtKey || jwtOpts.SigningKey.Length < 32)
+        throw new InvalidOperationException(
+            "Jwt:SigningKey must be set to a strong value (>= 32 chars) in Production.");
+
+    var bootstrap = ServiceAuth.ConnectorBootstrapKey(cfg);
+    if (bootstrap == ServiceAuth.DefaultBootstrapKey)
+        throw new InvalidOperationException(
+            "Seed:ConnectorBootstrapKey must be changed from the default in Production.");
+
+    var cloudAiKey = ServiceAuth.CloudAiServiceKey(cfg);
+    if (cloudAiKey == ServiceAuth.DefaultBootstrapKey)
+        throw new InvalidOperationException(
+            "CloudAi:ServiceKey must be set to a dedicated value in Production.");
+}
Relevance

⭐⭐ Medium

No prior review precedent requiring CloudAi key differ from connector bootstrap key; only
non-default checks seen.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Program.cs only checks cloudAiKey == DefaultBootstrapKey, while ServiceAuth resolves the cloud-ai
key to the connector bootstrap key when CloudAi:ServiceKey is missing; docker-compose further
defaults CloudAi__ServiceKey to CONNECTOR_BOOTSTRAP_KEY, making shared credentials likely.

backend/Program.cs[123-140]
backend/Auth/ServiceAuth.cs[9-16]
docker-compose.yml[82-86]

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

### Issue description
`ServiceAuth.CloudAiServiceKey()` falls back to `ConnectorBootstrapKey()` when `CloudAi:ServiceKey` is not set, but `Program.cs` only rejects the cloud-ai key when it equals the *default* bootstrap literal. In Production, this allows a configuration where both systems share a single (non-default) key.

### Issue Context
- The code comments and `.env.example` indicate cloud-ai should have a dedicated service key.
- `docker-compose.yml` also defaults `CloudAi__ServiceKey` to `${CONNECTOR_BOOTSTRAP_KEY}`, reinforcing the shared-key path.

### Fix Focus Areas
- backend/Program.cs[123-140]
- backend/Auth/ServiceAuth.cs[9-16]
- docker-compose.yml[82-86]

### Concrete fix
- In non-development environments, fail startup when `CloudAi:ServiceKey` is missing/blank.
- Additionally, fail when `CloudAi:ServiceKey` equals `Seed:ConnectorBootstrapKey` (resolved), not just when it equals `DefaultBootstrapKey`.
- Consider removing the `?? ConnectorBootstrapKey(cfg)` fallback entirely (or keep it only for Development).

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


2. Seeder masks migration failure 🐞 Bug ☼ Reliability
Description
DbSeeder catches all exceptions from MigrateAsync and silently falls back to EnsureCreatedAsync,
hiding real migration/schema failures (especially in production). This can leave the DB in an
incompatible state while startup continues, causing harder-to-diagnose runtime errors later.
Code

backend/Data/DbSeeder.cs[R11-18]

+        try
+        {
+            await db.Database.MigrateAsync();
+        }
+        catch
+        {
+            await db.Database.EnsureCreatedAsync();
+        }
Relevance

⭐⭐ Medium

No historical evidence on whether migration failures should abort startup vs fallback to
EnsureCreated.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
DbSeeder swallows all exceptions from MigrateAsync and does not log; Program.cs will then proceed as
if seeding succeeded, even though migrations may have failed and EnsureCreated may not repair the
schema.

backend/Data/DbSeeder.cs[11-18]
backend/Program.cs[144-158]

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 current seeding path suppresses any `MigrateAsync()` failure and attempts `EnsureCreatedAsync()` without logging. `EnsureCreatedAsync()` is not a safe fallback for a database that should be managed by migrations.

### Issue Context
`Program.cs` already retries `DbSeeder.SeedAsync` and logs readiness failures, but this catch prevents the real migration exception from surfacing.

### Fix Focus Areas
- backend/Data/DbSeeder.cs[11-18]
- backend/Program.cs[144-158]

### Concrete fix
- Catch only the specific legacy case you want to support (e.g., missing migrations history) and log the exception details.
- In Production, do not fall back to `EnsureCreatedAsync()`; log and rethrow so deployment fails fast.
- If supporting legacy EnsureCreated databases, implement an explicit migration bootstrap path (detect history table, then create it / baseline) rather than a blanket catch-all.

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



Remediation recommended

3. Zones 403 on missing 🐞 Bug ≡ Correctness
Description
ZonesController now returns Forbid() when the camera does not exist because AuthorizeCameraAsync
returns null for both missing and unauthorized cameras. This changes API semantics and prevents
clients from distinguishing invalid camera IDs from cross-tenant access denial.
Code

backend/Controllers/ZonesController.cs[R21-25]

+    {
+        var camera = await AuthorizeCameraAsync(cameraId);
+        if (camera is null) return Forbid();
+        return Ok(await _db.CameraZones.Where(z => z.CameraId == cameraId).ToListAsync());
+    }
Relevance

⭐⭐⭐ High

Team accepted NotFound vs Forbid distinction for missing vs unauthorized cameras (PR #4).

PR-#4

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
AuthorizeCameraAsync returns null when _db.Cameras.FindAsync returns null; List/Create then return
Forbid() for that null result, collapsing missing and unauthorized cases. This contradicts the
established pattern in prior fixes to distinguish NotFound vs Forbid for camera ownership checks.

backend/Controllers/ZonesController.cs[19-25]
backend/Controllers/ZonesController.cs[75-80]
backend/Auth/TenantAccess.cs[21-26]
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
`AuthorizeCameraAsync` returns `null` for missing cameras, and callers map `null` to `Forbid()`. That makes missing resources indistinguishable from authorization failures.

### Issue Context
Other areas in this repo explicitly preserve the NotFound vs Forbid distinction for cameras.

### Fix Focus Areas
- backend/Controllers/ZonesController.cs[19-25]
- backend/Controllers/ZonesController.cs[75-80]

### Concrete fix
- Change `AuthorizeCameraAsync` to return a tri-state result (e.g., `(Camera? camera, bool exists)` or an enum) so callers can return:
 - `NotFound()` when the camera does not exist
 - `Forbid()` when the camera exists but is not in an accessible store
- Apply consistently to List/Create/Update/Delete.

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


4. Heartbeat reports unreachable admin 🐞 Bug ≡ Correctness
Description
The connector heartbeat always reports an inferred LAN IP as AdminHost even when the admin UI binds
to 127.0.0.1 by default. The dashboard then uses this AdminHost/AdminPort to build snapshot URLs,
which will fail for remote dashboards and stores potentially misleading network metadata in the
backend.
Code

connector/app/workers.py[R60-90]

+    from .network_util import admin_public_host
+
+    admin_host = admin_public_host()
    while not stop.is_set():
        free = disk_free_pct(cfg.state_dir)
        state.disk_free_pct = free
-        degraded = None
+        operational = None
        if free < cfg.disk_critical_pct:
-            degraded = f"disk_critical:{free:.1f}%"
+            operational = f"disk_critical:{free:.1f}%"
        elif free < cfg.disk_warn_pct:
-            degraded = f"disk_warning:{free:.1f}%"
+            operational = f"disk_warning:{free:.1f}%"
        if store.pending_count() > 50:
-            degraded = (degraded + ";" if degraded else "") + "queue_backlog"
-        state.degraded_reason = degraded
+            operational = (operational + ";" if operational else "") + "queue_backlog"
+
+        with state._lock:
+            existing = state.degraded_reason
+            if existing and (
+                existing.startswith("Setup")
+                or "activation" in existing.lower()
+                or "pending" in existing.lower()
+            ):
+                degraded = f"{existing};{operational}" if operational else existing
+            else:
+                degraded = operational
+                state.degraded_reason = operational
+
        try:
-            client.heartbeat(free, store.pending_count(), degraded, cfg.version)
+            client.heartbeat(
+                free, store.pending_count(), degraded, cfg.version,
+                admin_host=admin_host, admin_port=cfg.admin_port,
+            )
Relevance

⭐⭐ Medium

No historical evidence found on reporting actual bind host vs inferred LAN IP in heartbeat metadata.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The connector binds the admin service to cfg.admin_bind_host (default loopback) but still reports
an inferred LAN IP to the backend; the dashboard then uses that stored host/port for snapshot links.

connector/app/workers.py[58-94]
connector/app/network_util.py[8-19]
connector/app/config.py[119-134]
connector/app/admin.py[903-906]
dashboard/src/app/pages/setup/setup.component.ts[485-495]

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

### Issue description
`run_heartbeat()` reports `admin_host = admin_public_host()` unconditionally, but the admin server commonly binds to `127.0.0.1`. This makes the backend/dash believe the admin is reachable at a LAN IP when it isn't.

### Issue Context
- Admin bind host defaults to loopback.
- Dashboard reads `Connector.adminHost/adminPort` and constructs snapshot URLs.

### Fix Focus Areas
- connector/app/workers.py[58-94]
- connector/app/network_util.py[8-19]
- connector/app/config.py[119-134]
- dashboard/src/app/pages/setup/setup.component.ts[485-495]

### Concrete fix
- Only include `adminHost/adminPort` in heartbeat when `cfg.admin_bind_host` is non-loopback (or when an explicit `CONNECTOR_ADMIN_PUBLIC_HOST` is set).
- Otherwise send `null`/omit those fields so the dashboard doesn’t attempt remote access.
- Optionally, include a separate boolean like `adminReachableLan=true/false` to help UI messaging.

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


5. Analytics loads alerts in-memory 🐞 Bug ➹ Performance
Description
AnalyticsController materializes all scoped alerts with ToListAsync and computes counts/grouping
in-process. This will become slow and memory-heavy as alert history grows, even though all metrics
can be computed via SQL aggregates.
Code

backend/Controllers/AnalyticsController.cs[R41-56]

+        var alertRows = await alerts.AsNoTracking().ToListAsync();
+        var clipCount = await clips.CountAsync();
+        var analyzedClips = await clips.CountAsync(c => c.Status == ClipStatus.Analyzed);
+
+        return Ok(new AnalyticsSummaryResponse(
+            TotalAlerts: alertRows.Count,
+            PendingAlerts: alertRows.Count(a => a.Status == AlertStatus.PendingReview),
+            HighRiskAlerts: alertRows.Count(a => a.RiskLevel == RiskLevel.High),
+            MediumRiskAlerts: alertRows.Count(a => a.RiskLevel == RiskLevel.Medium),
+            FalsePositives: alertRows.Count(a => a.Status == AlertStatus.FalsePositive),
+            TotalClips: clipCount,
+            AnalyzedClips: analyzedClips,
+            AlertsByType: alertRows
+                .GroupBy(a => a.AlertType)
+                .ToDictionary(g => g.Key, g => g.Count())
+        ));
Relevance

⭐⭐ Medium

No historical evidence found about enforcing SQL-aggregate vs in-memory analytics in controllers.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The controller explicitly uses ToListAsync() on the alerts query and then counts/groups in
application code, guaranteeing memory use proportional to total alerts.

backend/Controllers/AnalyticsController.cs[26-56]

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 summary endpoint loads all matching `Alert` rows into memory to compute totals and groupings, which scales poorly.

### Issue Context
This endpoint is intended for an ongoing dashboard view and will grow with time.

### Fix Focus Areas
- backend/Controllers/AnalyticsController.cs[26-56]

### Concrete fix
- Replace `ToListAsync` with EF aggregate queries:
 - `TotalAlerts = await alerts.CountAsync()`
 - `PendingAlerts = await alerts.CountAsync(a => a.Status == ...)`
 - `AlertsByType = await alerts.GroupBy(a => a.AlertType).Select(g => new { ... }).ToDictionaryAsync(...)`
- Consider running counts concurrently (multiple tasks) if the DB can handle it.

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


View more (1)
6. Bulk clip delete hides failures 🐞 Bug ☼ Reliability
Description
ClipsController bulk-delete returns an errors array but never populates it and does not isolate
per-clip exceptions, so a single S3/DB failure can abort the whole request with a 500. The response
also omits any accounting for NotFound clip IDs, making bulk results hard to interpret.
Code

backend/Controllers/ClipsController.cs[R276-297]

+        var deleted = 0;
+        var skipped = 0;
+        var errors = new List<string>();
+
+        foreach (var id in targetIds.Distinct())
+        {
+            var result = await TryDeleteClipAsync(id);
+            switch (result)
+            {
+                case DeleteClipResult.Deleted:
+                    deleted++;
+                    break;
+                case DeleteClipResult.SkippedConfirmed:
+                    skipped++;
+                    break;
+                case DeleteClipResult.NotFound:
+                    break;
+            }
+        }
+
+        return Ok(new BulkDeleteResponse(deleted, skipped, errors));
+    }
Relevance

⭐⭐ Medium

No historical evidence found on bulk-delete per-item error isolation/partial-failure reporting
conventions.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
BulkDelete allocates errors but never adds to it; TryDeleteClipAsync can throw (e.g., on S3
delete or SaveChangesAsync), which will bubble up and abort the loop.

backend/Controllers/ClipsController.cs[260-297]
backend/Controllers/ClipsController.cs[320-354]

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

### Issue description
Bulk delete currently does not report which clip IDs failed and can fail the entire operation on the first exception.

### Issue Context
`TryDeleteClipAsync` performs multiple external operations (S3 delete + DB changes) that can throw.

### Fix Focus Areas
- backend/Controllers/ClipsController.cs[260-297]
- backend/Controllers/ClipsController.cs[320-354]

### Concrete fix
- Wrap each `TryDeleteClipAsync(id)` call in try/catch; on exception append a human-readable entry to `errors` and continue.
- Track and return an explicit `notFound` count (or include a `missingIds` list) to make results deterministic.
- Optionally use a transaction for DB deletes per clip (keeping S3 deletion best-effort) and/or add a cancellation token to avoid long-running requests.

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



Informational

7. Route subscriptions not disposed 🐞 Bug ⚙ Maintainability
Description
AlertsComponent and ClipsComponent subscribe to ActivatedRoute.queryParamMap without disposing the
subscription, and ClipsComponent has no OnDestroy hook. Depending on component lifecycle/reuse, this
can retain component instances longer than intended and trigger redundant load() calls.
Code

dashboard/src/app/pages/alerts/alerts.component.ts[R261-265]

+    this.route.queryParamMap.subscribe((params) => {
+      const fromQuery = params.get('storeId');
+      if (fromQuery) this.storeId = fromQuery;
+      this.load();
+    });
Relevance

⭐⭐ Medium

No historical evidence found that reviewers enforce unsubscribing from Angular ActivatedRoute
subscriptions.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Both components create route subscriptions; AlertsComponent’s ngOnDestroy does not unsubscribe, and
ClipsComponent lacks ngOnDestroy entirely, so the subscriptions are unmanaged.

dashboard/src/app/pages/alerts/alerts.component.ts[259-273]
dashboard/src/app/pages/clips/clips.component.ts[345-367]

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

### Issue description
Direct `queryParamMap.subscribe(...)` calls are not disposed, which can lead to retained subscriptions and unexpected extra loads.

### Issue Context
AlertsComponent already implements `OnDestroy` but only closes SSE/timers; ClipsComponent does not implement `OnDestroy`.

### Fix Focus Areas
- dashboard/src/app/pages/alerts/alerts.component.ts[259-273]
- dashboard/src/app/pages/clips/clips.component.ts[345-367]

### Concrete fix
- Use `takeUntilDestroyed(inject(DestroyRef))` for both subscriptions, or store the Subscription and `unsubscribe()` in `ngOnDestroy`.
- Consider using the async pipe where practical (less manual lifecycle management).

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


8. LAN admin UI unauthenticated 🐞 Bug ⛨ Security
Description
When CONNECTOR_ADMIN_BIND_LAN=true, the admin server binds to 0.0.0.0 but authentication is only
enforced if CONNECTOR_ADMIN_TOKEN is non-empty. A misconfigured deployment can therefore expose
camera control/source-management endpoints to the shop LAN without any auth.
Code

connector/app/config.py[R128-133]

+        admin_token=os.getenv("CONNECTOR_ADMIN_TOKEN", "").strip(),
+        admin_bind_host=(
+            "0.0.0.0"
+            if os.getenv("CONNECTOR_ADMIN_BIND_LAN", "false").lower() == "true"
+            else os.getenv("CONNECTOR_ADMIN_BIND_HOST", "127.0.0.1").strip() or "127.0.0.1"
+        ),
Relevance

⭐ Low

Securing/binding connector admin to loopback/require auth was explicitly rejected in prior review
(PR #7).

PR-#7

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Config selects 0.0.0.0 when CONNECTOR_ADMIN_BIND_LAN=true, and the admin middleware skips auth
entirely when the token is empty, so LAN exposure can be unauthenticated.

connector/app/config.py[119-134]
connector/app/admin.py[54-65]
connector/app/admin.py[866-906]

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 admin API is effectively unauthenticated whenever `CONNECTOR_ADMIN_TOKEN` is empty. This PR also adds `CONNECTOR_ADMIN_BIND_LAN=true` which binds the admin service to `0.0.0.0`, enabling accidental LAN exposure without auth.

### Issue Context
- `admin_auth_middleware` only blocks requests when `admin_token` is set.
- Binding to `0.0.0.0` should be treated as a security-sensitive mode.

### Fix Focus Areas
- connector/app/config.py[119-134]
- connector/app/admin.py[54-65]

### Concrete fix
- If `cfg.admin_bind_host` is not loopback (e.g., `0.0.0.0` / non-127.0.0.1), require `cfg.admin_token` to be non-empty and reject startup otherwise.
- Alternatively, auto-generate a random token on first run and persist it to the local config store, and require it for all non-health endpoints.

ⓘ 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

Comment thread backend/Program.cs
Comment on lines +123 to +140
// ---- Production secret validation ----
const string DefaultJwtKey = "dev-super-secret-signing-key-change-me-please-32+";
if (!builder.Environment.IsDevelopment())
{
if (jwtOpts.SigningKey == DefaultJwtKey || jwtOpts.SigningKey.Length < 32)
throw new InvalidOperationException(
"Jwt:SigningKey must be set to a strong value (>= 32 chars) in Production.");

var bootstrap = ServiceAuth.ConnectorBootstrapKey(cfg);
if (bootstrap == ServiceAuth.DefaultBootstrapKey)
throw new InvalidOperationException(
"Seed:ConnectorBootstrapKey must be changed from the default in Production.");

var cloudAiKey = ServiceAuth.CloudAiServiceKey(cfg);
if (cloudAiKey == ServiceAuth.DefaultBootstrapKey)
throw new InvalidOperationException(
"CloudAi:ServiceKey must be set to a dedicated value in Production.");
}

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. Cloud-ai key not dedicated 🐞 Bug ⛨ Security

Production startup validation does not enforce CloudAi:ServiceKey being different from the connector
bootstrap key, and ServiceAuth.CloudAiServiceKey falls back to the connector key when unset. This
defeats the intended trust-boundary separation and allows the same credential to authorize both
cloud-ai service calls and connector registration.
Agent Prompt
### Issue description
`ServiceAuth.CloudAiServiceKey()` falls back to `ConnectorBootstrapKey()` when `CloudAi:ServiceKey` is not set, but `Program.cs` only rejects the cloud-ai key when it equals the *default* bootstrap literal. In Production, this allows a configuration where both systems share a single (non-default) key.

### Issue Context
- The code comments and `.env.example` indicate cloud-ai should have a dedicated service key.
- `docker-compose.yml` also defaults `CloudAi__ServiceKey` to `${CONNECTOR_BOOTSTRAP_KEY}`, reinforcing the shared-key path.

### Fix Focus Areas
- backend/Program.cs[123-140]
- backend/Auth/ServiceAuth.cs[9-16]
- docker-compose.yml[82-86]

### Concrete fix
- In non-development environments, fail startup when `CloudAi:ServiceKey` is missing/blank.
- Additionally, fail when `CloudAi:ServiceKey` equals `Seed:ConnectorBootstrapKey` (resolved), not just when it equals `DefaultBootstrapKey`.
- Consider removing the `?? ConnectorBootstrapKey(cfg)` fallback entirely (or keep it only for Development).

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

Comment thread connector/app/workers.py
Comment on lines +60 to +90
from .network_util import admin_public_host

admin_host = admin_public_host()
while not stop.is_set():
free = disk_free_pct(cfg.state_dir)
state.disk_free_pct = free
degraded = None
operational = None
if free < cfg.disk_critical_pct:
degraded = f"disk_critical:{free:.1f}%"
operational = f"disk_critical:{free:.1f}%"
elif free < cfg.disk_warn_pct:
degraded = f"disk_warning:{free:.1f}%"
operational = f"disk_warning:{free:.1f}%"
if store.pending_count() > 50:
degraded = (degraded + ";" if degraded else "") + "queue_backlog"
state.degraded_reason = degraded
operational = (operational + ";" if operational else "") + "queue_backlog"

with state._lock:
existing = state.degraded_reason
if existing and (
existing.startswith("Setup")
or "activation" in existing.lower()
or "pending" in existing.lower()
):
degraded = f"{existing};{operational}" if operational else existing
else:
degraded = operational
state.degraded_reason = operational

try:
client.heartbeat(free, store.pending_count(), degraded, cfg.version)
client.heartbeat(
free, store.pending_count(), degraded, cfg.version,
admin_host=admin_host, admin_port=cfg.admin_port,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Heartbeat reports unreachable admin 🐞 Bug ≡ Correctness

The connector heartbeat always reports an inferred LAN IP as AdminHost even when the admin UI binds
to 127.0.0.1 by default. The dashboard then uses this AdminHost/AdminPort to build snapshot URLs,
which will fail for remote dashboards and stores potentially misleading network metadata in the
backend.
Agent Prompt
### Issue description
`run_heartbeat()` reports `admin_host = admin_public_host()` unconditionally, but the admin server commonly binds to `127.0.0.1`. This makes the backend/dash believe the admin is reachable at a LAN IP when it isn't.

### Issue Context
- Admin bind host defaults to loopback.
- Dashboard reads `Connector.adminHost/adminPort` and constructs snapshot URLs.

### Fix Focus Areas
- connector/app/workers.py[58-94]
- connector/app/network_util.py[8-19]
- connector/app/config.py[119-134]
- dashboard/src/app/pages/setup/setup.component.ts[485-495]

### Concrete fix
- Only include `adminHost/adminPort` in heartbeat when `cfg.admin_bind_host` is non-loopback (or when an explicit `CONNECTOR_ADMIN_PUBLIC_HOST` is set).
- Otherwise send `null`/omit those fields so the dashboard doesn’t attempt remote access.
- Optionally, include a separate boolean like `adminReachableLan=true/false` to help UI messaging.

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

Comment on lines +21 to +25
{
var camera = await AuthorizeCameraAsync(cameraId);
if (camera is null) return Forbid();
return Ok(await _db.CameraZones.Where(z => z.CameraId == cameraId).ToListAsync());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Zones 403 on missing 🐞 Bug ≡ Correctness

ZonesController now returns Forbid() when the camera does not exist because AuthorizeCameraAsync
returns null for both missing and unauthorized cameras. This changes API semantics and prevents
clients from distinguishing invalid camera IDs from cross-tenant access denial.
Agent Prompt
### Issue description
`AuthorizeCameraAsync` returns `null` for missing cameras, and callers map `null` to `Forbid()`. That makes missing resources indistinguishable from authorization failures.

### Issue Context
Other areas in this repo explicitly preserve the NotFound vs Forbid distinction for cameras.

### Fix Focus Areas
- backend/Controllers/ZonesController.cs[19-25]
- backend/Controllers/ZonesController.cs[75-80]

### Concrete fix
- Change `AuthorizeCameraAsync` to return a tri-state result (e.g., `(Camera? camera, bool exists)` or an enum) so callers can return:
  - `NotFound()` when the camera does not exist
  - `Forbid()` when the camera exists but is not in an accessible store
- Apply consistently to List/Create/Update/Delete.

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

Comment thread backend/Data/DbSeeder.cs
Comment on lines +11 to +18
try
{
await db.Database.MigrateAsync();
}
catch
{
await db.Database.EnsureCreatedAsync();
}

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

5. Seeder masks migration failure 🐞 Bug ☼ Reliability

DbSeeder catches all exceptions from MigrateAsync and silently falls back to EnsureCreatedAsync,
hiding real migration/schema failures (especially in production). This can leave the DB in an
incompatible state while startup continues, causing harder-to-diagnose runtime errors later.
Agent Prompt
### Issue description
The current seeding path suppresses any `MigrateAsync()` failure and attempts `EnsureCreatedAsync()` without logging. `EnsureCreatedAsync()` is not a safe fallback for a database that should be managed by migrations.

### Issue Context
`Program.cs` already retries `DbSeeder.SeedAsync` and logs readiness failures, but this catch prevents the real migration exception from surfacing.

### Fix Focus Areas
- backend/Data/DbSeeder.cs[11-18]
- backend/Program.cs[144-158]

### Concrete fix
- Catch only the specific legacy case you want to support (e.g., missing migrations history) and log the exception details.
- In Production, do not fall back to `EnsureCreatedAsync()`; log and rethrow so deployment fails fast.
- If supporting legacy EnsureCreated databases, implement an explicit migration bootstrap path (detect history table, then create it / baseline) rather than a blanket catch-all.

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

Comment on lines +41 to +56
var alertRows = await alerts.AsNoTracking().ToListAsync();
var clipCount = await clips.CountAsync();
var analyzedClips = await clips.CountAsync(c => c.Status == ClipStatus.Analyzed);

return Ok(new AnalyticsSummaryResponse(
TotalAlerts: alertRows.Count,
PendingAlerts: alertRows.Count(a => a.Status == AlertStatus.PendingReview),
HighRiskAlerts: alertRows.Count(a => a.RiskLevel == RiskLevel.High),
MediumRiskAlerts: alertRows.Count(a => a.RiskLevel == RiskLevel.Medium),
FalsePositives: alertRows.Count(a => a.Status == AlertStatus.FalsePositive),
TotalClips: clipCount,
AnalyzedClips: analyzedClips,
AlertsByType: alertRows
.GroupBy(a => a.AlertType)
.ToDictionary(g => g.Key, g => g.Count())
));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

6. Analytics loads alerts in-memory 🐞 Bug ➹ Performance

AnalyticsController materializes all scoped alerts with ToListAsync and computes counts/grouping
in-process. This will become slow and memory-heavy as alert history grows, even though all metrics
can be computed via SQL aggregates.
Agent Prompt
### Issue description
The summary endpoint loads all matching `Alert` rows into memory to compute totals and groupings, which scales poorly.

### Issue Context
This endpoint is intended for an ongoing dashboard view and will grow with time.

### Fix Focus Areas
- backend/Controllers/AnalyticsController.cs[26-56]

### Concrete fix
- Replace `ToListAsync` with EF aggregate queries:
  - `TotalAlerts = await alerts.CountAsync()`
  - `PendingAlerts = await alerts.CountAsync(a => a.Status == ...)`
  - `AlertsByType = await alerts.GroupBy(a => a.AlertType).Select(g => new { ... }).ToDictionaryAsync(...)`
- Consider running counts concurrently (multiple tasks) if the DB can handle it.

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

Comment on lines +276 to +297
var deleted = 0;
var skipped = 0;
var errors = new List<string>();

foreach (var id in targetIds.Distinct())
{
var result = await TryDeleteClipAsync(id);
switch (result)
{
case DeleteClipResult.Deleted:
deleted++;
break;
case DeleteClipResult.SkippedConfirmed:
skipped++;
break;
case DeleteClipResult.NotFound:
break;
}
}

return Ok(new BulkDeleteResponse(deleted, skipped, errors));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

7. Bulk clip delete hides failures 🐞 Bug ☼ Reliability

ClipsController bulk-delete returns an errors array but never populates it and does not isolate
per-clip exceptions, so a single S3/DB failure can abort the whole request with a 500. The response
also omits any accounting for NotFound clip IDs, making bulk results hard to interpret.
Agent Prompt
### Issue description
Bulk delete currently does not report which clip IDs failed and can fail the entire operation on the first exception.

### Issue Context
`TryDeleteClipAsync` performs multiple external operations (S3 delete + DB changes) that can throw.

### Fix Focus Areas
- backend/Controllers/ClipsController.cs[260-297]
- backend/Controllers/ClipsController.cs[320-354]

### Concrete fix
- Wrap each `TryDeleteClipAsync(id)` call in try/catch; on exception append a human-readable entry to `errors` and continue.
- Track and return an explicit `notFound` count (or include a `missingIds` list) to make results deterministic.
- Optionally use a transaction for DB deletes per clip (keeping S3 deletion best-effort) and/or add a cancellation token to avoid long-running requests.

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

Comment on lines +261 to +265
this.route.queryParamMap.subscribe((params) => {
const fromQuery = params.get('storeId');
if (fromQuery) this.storeId = fromQuery;
this.load();
});

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

8. Route subscriptions not disposed 🐞 Bug ⚙ Maintainability

AlertsComponent and ClipsComponent subscribe to ActivatedRoute.queryParamMap without disposing the
subscription, and ClipsComponent has no OnDestroy hook. Depending on component lifecycle/reuse, this
can retain component instances longer than intended and trigger redundant load() calls.
Agent Prompt
### Issue description
Direct `queryParamMap.subscribe(...)` calls are not disposed, which can lead to retained subscriptions and unexpected extra loads.

### Issue Context
AlertsComponent already implements `OnDestroy` but only closes SSE/timers; ClipsComponent does not implement `OnDestroy`.

### Fix Focus Areas
- dashboard/src/app/pages/alerts/alerts.component.ts[259-273]
- dashboard/src/app/pages/clips/clips.component.ts[345-367]

### Concrete fix
- Use `takeUntilDestroyed(inject(DestroyRef))` for both subscriptions, or store the Subscription and `unsubscribe()` in `ngOnDestroy`.
- Consider using the async pipe where practical (less manual lifecycle management).

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

@coderabbitai coderabbitai Bot mentioned this pull request Jul 29, 2026
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