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
42 changes: 42 additions & 0 deletions db/migrations/0051_add_camera_collections.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
-- Saved static and selector-backed smart camera collections.

-- migrate:up

CREATE TABLE camera_collections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uuid TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
collection_type TEXT NOT NULL CHECK (collection_type IN ('static', 'smart')),
selector_json TEXT NOT NULL DEFAULT '',
is_shared INTEGER NOT NULL DEFAULT 1,
owner_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);

CREATE UNIQUE INDEX idx_camera_collections_name
ON camera_collections(name COLLATE NOCASE);

CREATE INDEX idx_camera_collections_owner
ON camera_collections(owner_user_id, is_shared);

CREATE TABLE camera_collection_members (
collection_uuid TEXT NOT NULL
REFERENCES camera_collections(uuid) ON DELETE CASCADE,
camera_uuid TEXT NOT NULL REFERENCES streams(camera_uuid) ON DELETE CASCADE,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
PRIMARY KEY (collection_uuid, camera_uuid)
);

CREATE INDEX idx_camera_collection_members_camera
ON camera_collection_members(camera_uuid, collection_uuid);

-- migrate:down

DROP INDEX IF EXISTS idx_camera_collection_members_camera;
DROP TABLE IF EXISTS camera_collection_members;
DROP INDEX IF EXISTS idx_camera_collections_owner;
DROP INDEX IF EXISTS idx_camera_collections_name;
DROP TABLE IF EXISTS camera_collections;
SELECT 1;
70 changes: 70 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,76 @@ Accepts the same request as the query endpoint, caps pages at 50 cameras, and
adds `matched_clauses` to each returned camera. An optional `camera_uuid`
restricts the preview to one camera.

### Camera Collections

Collections are durable named camera groups. A `static` collection stores UUID
membership; a `smart` collection stores a selector v1 object and updates as
cameras, locations, tags, configuration, or health change.

#### List and Create Collections

```
GET /api/camera-collections
POST /api/camera-collections
```

Listing requires viewer access and returns only shared collections, collections
owned by the caller, or all collections for administrators. Counts are computed
after current tag RBAC. Smart selector definitions are returned only to an
administrator or the collection owner; other viewers receive `selector: null`
and `selector_redacted: true`. Creation is administrator-only.

```json
{
"name": "Offline entrances",
"description": "Entrance cameras requiring attention",
"type": "smart",
"shared": true,
"selector": {
"version": 1,
"expression": {
"op": "and",
"children": [
{"op": "tag_any", "uuids": ["entrance-tag-uuid"]},
{"op": "health", "values": ["down"]}
]
}
}
}
```

#### Read, Update, and Delete a Collection

```
GET /api/camera-collections/{collection_uuid}
PUT /api/camera-collections/{collection_uuid}
DELETE /api/camera-collections/{collection_uuid}
```

Reads follow collection visibility and camera RBAC. Update and delete are
administrator-only in this initial phase. Switching a collection to `smart`
atomically removes obsolete static membership.

#### Static Collection Members

```
GET /api/camera-collections/{collection_uuid}/members
PUT /api/camera-collections/{collection_uuid}/members
```

`PUT` replaces membership atomically with a `camera_uuids` array and is limited
to 4,096 entries. `GET` omits cameras outside the caller's current scope. Smart
collections reject explicit member operations.

#### Preview a Collection

```
POST /api/camera-collections/{collection_uuid}/preview
```

Returns the authorized `matched_count` and a sample of at most 50 camera UUIDs,
names, and location paths.

### System

#### Get System Information
Expand Down
55 changes: 55 additions & 0 deletions include/database/db_camera_collections.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#ifndef LIGHTNVR_DB_CAMERA_COLLECTIONS_H
#define LIGHTNVR_DB_CAMERA_COLLECTIONS_H

#include <stdbool.h>
#include <stdint.h>

#include "core/config.h"

#define CAMERA_COLLECTION_NAME_MAX 128
#define CAMERA_COLLECTION_DESCRIPTION_MAX 512
#define CAMERA_COLLECTION_TYPE_MAX 16
#define CAMERA_COLLECTION_SELECTOR_MAX 8192
#define CAMERA_COLLECTION_MAX_MEMBERS 4096

typedef struct {
char uuid[CAMERA_UUID_STRING_SIZE];
char name[CAMERA_COLLECTION_NAME_MAX];
char description[CAMERA_COLLECTION_DESCRIPTION_MAX];
char collection_type[CAMERA_COLLECTION_TYPE_MAX];
char selector_json[CAMERA_COLLECTION_SELECTOR_MAX];
bool is_shared;
int64_t owner_user_id;
int member_count;
int64_t created_at;
int64_t updated_at;
} camera_collection_t;

typedef enum {
DB_CAMERA_COLLECTION_OK = 0,
DB_CAMERA_COLLECTION_NOT_FOUND = -1,
DB_CAMERA_COLLECTION_CONFLICT = -2,
DB_CAMERA_COLLECTION_INVALID = -3,
DB_CAMERA_COLLECTION_ERROR = -4,
DB_CAMERA_COLLECTION_WRONG_TYPE = -5,
DB_CAMERA_COLLECTION_LIMIT = -6
} db_camera_collection_result_t;

int db_camera_collection_count(void);
int db_camera_collection_list(camera_collection_t *collections, int max_count);
db_camera_collection_result_t db_camera_collection_get(
const char *uuid, camera_collection_t *collection);
db_camera_collection_result_t db_camera_collection_create(
camera_collection_t *collection);
db_camera_collection_result_t db_camera_collection_update(
camera_collection_t *collection);
db_camera_collection_result_t db_camera_collection_delete(const char *uuid);

int db_camera_collection_list_members(
const char *collection_uuid,
char camera_uuids[][CAMERA_UUID_STRING_SIZE], int max_count);
db_camera_collection_result_t db_camera_collection_set_members(
const char *collection_uuid, const char *const *camera_uuids,
int camera_count);

#endif /* LIGHTNVR_DB_CAMERA_COLLECTIONS_H */
47 changes: 46 additions & 1 deletion include/database/db_embedded_migrations.h
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,44 @@ static const char migration_0050_down[] =
"DROP TABLE IF EXISTS camera_tags;\n"
"SELECT 1;";

static const char migration_0051_up[] =
"CREATE TABLE camera_collections (\n"
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
" uuid TEXT NOT NULL UNIQUE,\n"
" name TEXT NOT NULL,\n"
" description TEXT NOT NULL DEFAULT '',\n"
" collection_type TEXT NOT NULL CHECK (collection_type IN ('static', 'smart')),\n"
" selector_json TEXT NOT NULL DEFAULT '',\n"
" is_shared INTEGER NOT NULL DEFAULT 1,\n"
" owner_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,\n"
" created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n"
" updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n"
");\n"
"\n"
"CREATE UNIQUE INDEX idx_camera_collections_name\n"
"ON camera_collections(name COLLATE NOCASE);\n"
"\n"
"CREATE INDEX idx_camera_collections_owner\n"
"ON camera_collections(owner_user_id, is_shared);\n"
"\n"
"CREATE TABLE camera_collection_members (\n"
" collection_uuid TEXT NOT NULL REFERENCES camera_collections(uuid) ON DELETE CASCADE,\n"
" camera_uuid TEXT NOT NULL REFERENCES streams(camera_uuid) ON DELETE CASCADE,\n"
" created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n"
" PRIMARY KEY (collection_uuid, camera_uuid)\n"
");\n"
"\n"
"CREATE INDEX idx_camera_collection_members_camera\n"
"ON camera_collection_members(camera_uuid, collection_uuid);";

static const char migration_0051_down[] =
"DROP INDEX IF EXISTS idx_camera_collection_members_camera;\n"
"DROP TABLE IF EXISTS camera_collection_members;\n"
"DROP INDEX IF EXISTS idx_camera_collections_owner;\n"
"DROP INDEX IF EXISTS idx_camera_collections_name;\n"
"DROP TABLE IF EXISTS camera_collections;\n"
"SELECT 1;";

static const migration_t embedded_migrations_data[] = {
{
.version = "0001",
Expand Down Expand Up @@ -1128,8 +1166,15 @@ static const migration_t embedded_migrations_data[] = {
.sql_down = migration_0050_down,
.is_embedded = true
},
{
.version = "0051",
.description = "add_camera_collections",
.sql_up = migration_0051_up,
.sql_down = migration_0051_down,
.is_embedded = true
},
};

#define EMBEDDED_MIGRATIONS_COUNT 49
#define EMBEDDED_MIGRATIONS_COUNT 50

#endif /* DB_EMBEDDED_MIGRATIONS_H */
4 changes: 4 additions & 0 deletions include/database/db_fleet_query.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@
* returned array and must free it. Zero cameras returns success with NULL. */
int db_fleet_camera_load(fleet_camera_t **cameras, int *count);

/* Add the current in-process health snapshot to a loaded inventory. Cameras
* without an active metrics slot remain unknown; disabled cameras stay disabled. */
void fleet_camera_enrich_runtime_health(fleet_camera_t *cameras, int count);

#endif /* LIGHTNVR_DB_FLEET_QUERY_H */
23 changes: 23 additions & 0 deletions include/web/api_handlers_camera_collections.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#ifndef LIGHTNVR_API_HANDLERS_CAMERA_COLLECTIONS_H
#define LIGHTNVR_API_HANDLERS_CAMERA_COLLECTIONS_H

#include "web/request_response.h"

void handle_get_camera_collections(const http_request_t *req,
http_response_t *res);
void handle_post_camera_collection(const http_request_t *req,
http_response_t *res);
void handle_get_camera_collection(const http_request_t *req,
http_response_t *res);
void handle_put_camera_collection(const http_request_t *req,
http_response_t *res);
void handle_delete_camera_collection(const http_request_t *req,
http_response_t *res);
void handle_get_camera_collection_members(const http_request_t *req,
http_response_t *res);
void handle_put_camera_collection_members(const http_request_t *req,
http_response_t *res);
void handle_post_camera_collection_preview(const http_request_t *req,
http_response_t *res);

#endif /* LIGHTNVR_API_HANDLERS_CAMERA_COLLECTIONS_H */
Loading
Loading