Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ CONNECTOR_BOOTSTRAP_KEY=dev-connector-bootstrap-key
# Dedicated key for cloud-ai worker (set separately in production)
CLOUD_AI_SERVICE_KEY=dev-cloud-ai-service-key
# Must match connector/installer/onevo-connector.iss AppVersion and connector app/config.py version
CONNECTOR_INSTALLER_VERSION=1.1.5
CONNECTOR_INSTALLER_URL=https://installer-site-one.vercel.app/ONEVO-Connector-Setup-1.1.5.exe
CONNECTOR_INSTALLER_SIZE_BYTES=98067717
CONNECTOR_INSTALLER_SHA256=4612e540532d54ae5940e24e62d207a60e4d3d1cc74bcad14f91c6b5e86cb7c4
CONNECTOR_INSTALLER_VERSION=1.1.12
CONNECTOR_INSTALLER_URL=https://gofnzdebgqmelkd8.public.blob.vercel-storage.com/releases/ONEVO-Connector-Setup-1.1.12.exe
CONNECTOR_INSTALLER_SIZE_BYTES=129692275
CONNECTOR_INSTALLER_SHA256=1dfbd51a48b8f295ad42b3b26e5f9249205cfa1f73663166025388504c701767

# ---- Pilot behavior ----
# silent = alerts stored but not surfaced; manager_only = only managers see; all = everyone
Expand Down
1 change: 1 addition & 0 deletions backend/Contracts/Dtos.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public record StoreOverviewResponse(
// ---- Cameras ----
public record CreateCameraRequest(Guid StoreId, string Name, string RtspUrl, string? OnvifHost, int? OnvifPort);
public record UpdateCameraRequest(string? Name, string? RtspUrl, string? Status, string? OnvifHost, int? OnvifPort);
public record BulkDisableCamerasRequest(List<Guid> CameraIds);
public record UpdateDeviceInfoRequest(
string? Manufacturer,
string? Model,
Expand Down
26 changes: 26 additions & 0 deletions backend/Controllers/CamerasController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,32 @@ public async Task<IActionResult> Update(Guid id, UpdateCameraRequest req)
return Ok(cam);
}

[HttpDelete("{id:guid}")]
[Authorize(Roles = "Admin,Manager,Installer")]
public async Task<IActionResult> Disable(Guid id)
{
var cam = await _db.Cameras.FindAsync(id);
if (cam is null) return NotFound();
if (!TenantAccess.CanAccessStore(User, cam.StoreId)) return Forbid();
cam.Status = CameraStatus.Disabled;
await _db.SaveChangesAsync();
return Ok(new { ok = true, cameraId = cam.Id });
}
Comment on lines +91 to +101

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. Disabled cameras can’t re-enable 🐞 Bug ≡ Correctness

DELETE /api/cameras/{id} sets Status=Disabled, but camera provisioning/upsert does not reset Status
on sourceKey conflicts, so re-adding the same source later keeps the camera Disabled. Because
connector camera retrieval excludes Disabled cameras, the connector will never resume monitoring
that camera without a separate manual status fix.
Agent Prompt
## Issue description
A camera can be put into `CameraStatus.Disabled`, and the connector is then prevented from seeing it. However, when the same physical source is provisioned again (same `sourceKey`), the backend upsert path updates name/URLs but does not change `Status`, leaving the camera permanently disabled from the connector’s perspective.

## Issue Context
- Disabling a camera is implemented as a status change, not a delete.
- Connector camera fetch explicitly filters out `Disabled`.
- Provisioning uses `ON CONFLICT (ConnectorId, SourceKey) DO UPDATE` but does not update `Status`.

## Fix Focus Areas
- backend/Controllers/CamerasController.cs[91-115]
- backend/Services/CameraProvisioningService.cs[52-67]
- backend/Controllers/ConnectorsController.cs[213-216]

### Implementation guidance
Pick one consistent behavior:
1) **Re-enable on reprovision**: In `CameraProvisioningService.ProvisionConnectorCameraAsync`, update `Status` in the `DO UPDATE` clause *at least when the existing row is Disabled* (e.g., set to Pending/Active).
2) **Disable = detach**: In the disable endpoint, also clear `ConnectorId` and/or `SourceKey` so provisioning creates a fresh active row (only if that’s acceptable for historical continuity).
3) **Explicit enable**: Add an enable endpoint / UI flow that sets status back to Active/Pending and ensure connector can see it again.

Add a regression test for: disable camera → provision same `sourceKey` again → camera becomes visible to `/api/connectors/cameras`.

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


[HttpPost("bulk-disable")]
[Authorize(Roles = "Admin,Manager,Installer")]
public async Task<IActionResult> BulkDisable(BulkDisableCamerasRequest req)
{
var ids = (req.CameraIds ?? []).Distinct().ToList();
if (ids.Count == 0) return BadRequest(new { error = "Select at least one camera" });
var cameras = await TenantAccess.ScopeCameras(_db.Cameras, User)
.Where(c => ids.Contains(c.Id))
.ToListAsync();
foreach (var camera in cameras) camera.Status = CameraStatus.Disabled;
await _db.SaveChangesAsync();
return Ok(new { ok = true, disabled = cameras.Count });
}

// Called by the connector after ONVIF query — stores device identity in the DB.
[HttpPut("{id:guid}/device-info")]
[AllowAnonymous] // authenticated by connector's X-Connector-Key header (checked below)
Expand Down
39 changes: 38 additions & 1 deletion backend/Controllers/ConnectorsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,23 @@ public async Task<ActionResult<ClaimSetupCodeResponse>> Claim(ClaimSetupCodeRequ
if (row is null)
return BadRequest(new { error = "Invalid, used, or expired setup code" });

// A store owns one connector identity. Do not let a second PC claim a
// code and rotate the active shop connector's API key. Re-pairing is
// allowed only after uninstall/offline timeout.
var activeCutoff = DateTimeOffset.UtcNow.AddMinutes(-2);
var activeConnector = await _db.Connectors
.AsNoTracking()
.AnyAsync(c =>
c.StoreId == row.StoreId &&
c.LastHeartbeat >= activeCutoff &&
(c.Status == ConnectorStatus.Healthy ||
c.Status == ConnectorStatus.Degraded),
HttpContext.RequestAborted);
if (activeConnector)
return Conflict(new {
error = "This store already has an active connector. Uninstall or stop it before pairing another PC."
});

await using var transaction = await _db.Database.BeginTransactionAsync(
HttpContext.RequestAborted);

Expand Down Expand Up @@ -144,6 +161,25 @@ public async Task<IActionResult> Heartbeat(HeartbeatRequest req)
return Ok(new { ok = true });
}

// Called by the Windows uninstaller before it removes local credentials.
// The connector row is retained so a reinstall can safely re-pair the same
// store and cameras, but the dashboard stops treating it as installed.
[AllowAnonymous]
[HttpPost("uninstall")]
public async Task<IActionResult> Uninstall()
{
var connector = await _connectorAuth.AuthenticateAsync(
Request, HttpContext.RequestAborted);
if (connector is null) return Unauthorized();

connector.Status = ConnectorStatus.Offline;
connector.LastHeartbeat = null;
connector.DegradedReason = "uninstalled";
connector.UploadQueueDepth = 0;
await _db.SaveChangesAsync(HttpContext.RequestAborted);
return Ok(new { ok = true });
}

// Health list for the dashboard.
[Authorize]
[HttpGet]
Expand Down Expand Up @@ -180,7 +216,8 @@ await _cameraProvisioning.EnsureDemoZonesAsync(

var cameras = await _db.Cameras
.Where(c => c.StoreId == connector.StoreId &&
c.ConnectorId == connector.Id)
c.ConnectorId == connector.Id &&
c.Status != CameraStatus.Disabled)
.OrderBy(c => c.Name)
.Select(c => new
{
Expand Down
3 changes: 2 additions & 1 deletion backend/Domain/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public enum CameraStatus
Pending,
Active,
AnalyticsOnly,
Offline
Offline,
Disabled
}

public enum ConnectorStatus
Expand Down
2 changes: 1 addition & 1 deletion backend/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"BaseUrl": "http://localhost:4200"
},
"ConnectorInstaller": {
"Version": "1.1.5",
"Version": "1.1.12",
"Path": "../installer-site"
}
}
Loading
Loading