A simple development page to visualize line-crossing alerts from MongoDB with bounding box overlays.
- đź“‹ View all line-crossing alerts from MongoDB
- 🖼️ Display images with bounding box overlays
- 🔍 Filter alerts by camera, location, status, and date
- đź“„ Pagination support for large datasets
- 🎨 Color-coded bounding boxes by detection type
- 🔍 Full-size image viewer with detailed metadata
- Node.js 16+ installed
- MongoDB instance running
- Access to mediaservice for images and metadata
-
Install dependencies:
npm install
-
Configure environment:
cp .env.example .env
Edit
.envand set your configuration:MONGODB_URI=mongodb://localhost:27017/historian MEDIA_SERVICE_BASE_URL=https://teknoir.cloud/victra-poc/media-service/api PORT=3000 -
Run the dev server:
npm run dev
-
Open in browser:
http://localhost:3000
/
├── server.js # Express server entry point
├── package.json # Dependencies and scripts
├── .env.example # Environment variables template
├── routes/
│ └── alerts.js # API route handlers
├── utils/
│ └── db.js # MongoDB connection utility
└── public/ # Static frontend files
├── index.html # Main page
├── css/
│ └── style.css # Styling
└── js/
├── app.js # Main app logic
├── api.js # API client
└── canvas.js # Canvas drawing utilities
Health check endpoint.
Response:
{
"status": "ok",
"timestamp": "2025-10-29T12:00:00.000Z"
}List all alerts with pagination and filtering.
Query Parameters:
page- Page number (default: 1)limit- Items per page (default: 20)cameraId- Filter by camera IDlocation- Filter by locationstatus- Filter by status (new, reviewed)fromDate- Filter from date (ISO 8601)toDate- Filter to date (ISO 8601)
Response:
{
"alerts": [...],
"pagination": {
"page": 1,
"limit": 20,
"total": 100,
"pages": 5
}
}Get specific alert details.
Query Params:
enrich=0to disable enrichmentraw=1ordebug=1to include raw line-crossing document underenrichment.raw
Enrichment Fields (when available):
{
"enrichment": {
"parsed": {"detectionId": "...", "direction": "entry|exit", "segmentIndex": 0},
"lineDirection": "entry",
"lineId": "lc-entry-0-segments",
"classifiers": [{"label": "up", "score": 0.9873}],
"pose": {"coords": [...], "skeleton": [...], "keypoints": [...]},
"burstImages": ["https://.../jpeg/media/lc-person-cutouts/...-0.jpg", "https://.../jpeg/...-1.jpg"],
"cutoutImage": "https://.../jpeg/media/lc-person-cutouts/...cutout.jpg"
}
}Response:
{
"_id": "690178525f8e8dd7d5813f51",
"id": "nc0009-salefloor-270-155f0f2e-935-lc-exit-0",
"type": "line_crossing",
"start_time": "2025-10-29T02:13:25.995Z",
"end_time": "2025-10-29T02:13:35.995Z",
"peripheral_id": "nc0009-salefloor-270",
"from_device": "victra-poc-02",
"detection_id": "nc0009-salefloor-270-155f0f2e-935",
"video_snapshot": "media/snapshots/nc0009-salefloor-270/line-crossing/2025-10-29T02-13-30.841Z.jpeg",
"video_url": "media/videos/nc0009-salefloor-270/line-crossing/2025-10-29T02-13-30.841Z.mp4",
"annotations_url": "media/annotations/nc0009-salefloor-270/line-crossing/2025-10-29T02-13-30.841Z.json",
"status": "new",
"label": "person",
"imageUrl": "https://teknoir.cloud/victra-poc/media-service/api/media/snapshots/...",
"videoUrl": "https://teknoir.cloud/victra-poc/media-service/api/media/videos/...",
"metadataUrl": "https://teknoir.cloud/victra-poc/media-service/api/media/annotations/..."
}Use the ?debug=1 query param on the alert details endpoint to see each query strategy and whether it matched:
Example:
GET /api/alerts/ALERT_OBJECT_ID?debug=1
Response enrichment debug structure:
"enrichment": {
"debug": {
"searchedFor": {"detectionId": "nc0009-salefloor-270-155f0f2e-935", "direction": "exit", "segmentIndex": 0},
"peripheral_id": "nc0009-salefloor-270",
"attempts": [
{"strategy": "exact_match", "found": false, "query": {"metadata.annotations.teknoir.org/linedir": "exit", "data.id": "nc0009-salefloor-270-155f0f2e-935"}},
{"strategy": "prefix_regex", "found": true, "query": {"metadata.annotations.teknoir.org/linedir": "exit", "data.id": {"$regex": "^nc0009-salefloor-270-155f0f2e-935"}}},
{"strategy": "lineid+peripheral", "found": false, "query": {"metadata.annotations.teknoir.org/linedir": "exit", "metadata.annotations.teknoir.org/lineid": "lc-exit-0-segments"}},
{"strategy": "recent_by_peripheral_id", "candidates": 0, "query": {"metadata.annotations.teknoir.org/linedir": "exit", "data.peripheral.id": "nc0009-salefloor-270"}},
{"strategy": "recent_by_peripheral_name", "candidates": 0, "query": {"metadata.annotations.teknoir.org/linedir": "exit", "data.peripheral.name": "nc0009-salefloor-270"}},
{"strategy": "latest_direction", "found": true, "query": {"metadata.annotations.teknoir.org/linedir": "exit"}}
],
"prettyPrintedAttempts": [ /* Same content but safe-cloned */ ]
}
}The application filters for alerts with type: "line_crossing" only.
Alerts Collection - Fields Used:
{
_id: ObjectId,
type: "line_crossing", // Filter: only line_crossing alerts
start_time: ISODate, // Alert start timestamp
end_time: ISODate, // Alert end timestamp
from_device: String, // Device ID (e.g., "victra-poc-02")
peripheral_id: String, // Camera ID (e.g., "nc0009-salefloor-270")
detection_id: String, // Unique detection identifier
video_snapshot: String, // Path to image snapshot
video_url: String, // Path to video file
annotations_url: String, // Path to metadata JSON
status: String, // "new", "reviewed", etc.
// ... other fields available in full document
}Line-Crossings Collection - Example:
{
"_id": {"$oid": "690340081e686e6ca09ebeb6"},
"apiVersion": "teknoir.org/v1beta2",
"data": {
"id": "demonstrator-se-lc1-44b1357b-2988",
"x1": 0.55859375,
"y1": 0.15927734375,
"x2": 0.61796875,
"y2": 0.448828125,
"width": 0.059375,
"height": 0.28955078125,
"area": 0.01719207763671875,
"ratio": 2.743112664473684,
"x_center": 0.58828125,
"y_center": 0.304052734375,
"score": 0.8896484375,
"class_id": 4,
"label": "line-crossing",
"keypoints": ["nose", "left_eye", "right_eye", "left_ear", "right_ear", "left_shoulder", "right_shoulder", "left_elbow", "right_elbow", "left_hand", "right_hand", "left_hip", "right_hip", "left_knee", "right_knee", "left_ankle", "right_ankle", "neck"],
"coords": [
[0.5995446443557739, 0.20032939314842224],
[0.6011298298835754, 0.19538627564907074],
[0.597865879535675, 0.19041940569877625],
[0, 0],
[0.5884963274002075, 0.18590237200260162],
[0.5982832908630371, 0.2297966182231903],
[0.5865501761436462, 0.2297087013721466],
[0, 0],
[0.5949140191078186, 0.28068432211875916],
[0, 0],
[0.610177218914032, 0.3156489431858063],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0.5917493104934692, 0.23194950819015503]
],
"skeleton": [
[0, 17],
[0, 1],
[0, 2],
[1, 3],
[2, 4],
[17, 5],
[17, 6],
[17, 11],
[17, 12],
[5, 7],
[7, 9],
[6, 8],
[8, 10],
[11, 12],
[11, 13],
[13, 15],
[12, 14],
[14, 16],
[0, 0]
],
"classifiers": [
{
"label": "up",
"score": 0.9873543381690979
}
],
"timestamp": "2025-10-30T10:37:17.439Z",
"type": "analytics",
"in_exclusion_zone": false,
"in_inclusion_zone": false,
"peripheral": {
"type": "camera",
"name": "demonstrator-se-lc1",
"id": "demonstrator-se-lc1"
},
"filename": "media/lc-person-cutouts/2025-10-30/lc-entry-0-segments/demonstrator-se-lc1-44b1357b-2988-2025-10-30T10:37:57.012Z.jpg",
"burst": ["media/lc-person-cutouts/2025-10-30/lc-entry-0-segments/demonstrator-se-lc1-44b1357b-2988-2025-10-30T10:37:57.012Z-0.jpg", "media/lc-person-cutouts/2025-10-30/lc-entry-0-segments/demonstrator-se-lc1-44b1357b-2988-2025-10-30T10:37:57.012Z-1.jpg", "media/lc-person-cutouts/2025-10-30/lc-entry-0-segments/demonstrator-se-lc1-44b1357b-2988-2025-10-30T10:37:57.012Z-2.jpg", "media/lc-person-cutouts/2025-10-30/lc-entry-0-segments/demonstrator-se-lc1-44b1357b-2988-2025-10-30T10:37:57.012Z-3.jpg", "media/lc-person-cutouts/2025-10-30/lc-entry-0-segments/demonstrator-se-lc1-44b1357b-2988-2025-10-30T10:37:57.012Z-4.jpg", "media/lc-person-cutouts/2025-10-30/lc-entry-0-segments/demonstrator-se-lc1-44b1357b-2988-2025-10-30T10:37:57.012Z-5.jpg", "media/lc-person-cutouts/2025-10-30/lc-entry-0-segments/demonstrator-se-lc1-44b1357b-2988-2025-10-30T10:37:57.012Z-6.jpg", "media/lc-person-cutouts/2025-10-30/lc-entry-0-segments/demonstrator-se-lc1-44b1357b-2988-2025-10-30T10:37:57.012Z-7.jpg", "media/lc-person-cutouts/2025-10-30/lc-entry-0-segments/demonstrator-se-lc1-44b1357b-2988-2025-10-30T10:37:57.012Z-8.jpg", "media/lc-person-cutouts/2025-10-30/lc-entry-0-segments/demonstrator-se-lc1-44b1357b-2988-2025-10-30T10:37:57.012Z-9.jpg"]
},
"kind": "DetectionEvent",
"metadata": {
"id": "demonstrator-se-lc1-44b1357b-2988",
"namespace": "test-namespace",
"timestamp": "2025-10-30T10:37:17.830Z",
"annotations": {
"teknoir.org/linedir": "entry",
"teknoir.org/lineid": "lc-entry-0-segments"
}
}
}Media URLs:
The application constructs full URLs by prepending MEDIA_SERVICE_BASE_URL to the paths:
- Image:
${MEDIA_SERVICE_BASE_URL}/${video_snapshot} - Video:
${MEDIA_SERVICE_BASE_URL}/${video_url} - Metadata:
${MEDIA_SERVICE_BASE_URL}/${annotations_url}
Bounding Box Metadata JSON (from mediaservice):
{
"data": {
"metadata": {
"start_time": "2025-10-15T10:50:48.242Z",
"end_time": "2025-10-15T10:51:33.144Z"
},
"detections": [
{
"id": "demonstrator-se-lc2-9749d32f-2601",
"x1": 0.2462890625,
"y1": 0.4171875,
"x2": 0.2703125,
"y2": 0.471484375,
"width": 0.0240234375,
"height": 0.054296875,
"area": 0.0013043975830078127,
"ratio": 1.271341463414634,
"x_center": 0.25830078125,
"y_center": 0.4443359375,
"score": 0.63037109375,
"class_id": 3,
"label": "face_cover",
"timestamp": "2025-10-15T10:50:48.242Z",
"type": "object",
"in_exclusion_zone": false,
"in_inclusion_zone": false,
"peripheral": {
"type": "camera",
"name": "demonstrator-se-lc2",
"id": "demonstrator-se-lc2",
"stream_uri": "rtsp://mediamtx:8554/front-door-1-wrongly-classified-exits"
}
},
{
"id": "demonstrator-se-lc2-9749d32f-2599",
"x1": 0.2236328125,
"y1": 0.3982421875,
"x2": 0.312109375,
"y2": 0.740234375,
"width": 0.0884765625,
"height": 0.3419921875,
"area": 0.030258293151855464,
"ratio": 2.174254966887417,
"x_center": 0.26787109375,
"y_center": 0.56923828125,
"score": 0.9267578125,
"class_id": 4,
"label": "person",
"keypoints": [
"nose",
"left_eye",
"right_eye",
"left_ear",
"right_ear",
"left_shoulder",
"right_shoulder",
"left_elbow",
"right_elbow",
"left_hand",
"right_hand",
"left_hip",
"right_hip",
"left_knee",
"right_knee",
"left_ankle",
"right_ankle",
"neck"
],
"coords": [
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0.2863526940345764,
0.6201004981994629
],
[
0,
0
],
[
0,
0
],
[
0,
0
]
],
"skeleton": [
[
0,
17
],
[
0,
1
],
[
0,
2
],
[
1,
3
],
[
2,
4
],
[
17,
5
],
[
17,
6
],
[
17,
11
],
[
17,
12
],
[
5,
7
],
[
7,
9
],
[
6,
8
],
[
8,
10
],
[
11,
12
],
[
11,
13
],
[
13,
15
],
[
12,
14
],
[
14,
16
],
[
0,
0
]
],
"classifiers": [
{
"label": "up",
"score": 0.9802155494689941
}
],
"timestamp": "2025-10-15T10:50:48.242Z",
"type": "object",
"in_exclusion_zone": false,
"in_inclusion_zone": false,
"peripheral": {
"type": "camera",
"name": "demonstrator-se-lc2",
"id": "demonstrator-se-lc2",
"stream_uri": "rtsp://mediamtx:8554/front-door-1-wrongly-classified-exits"
}
}
]
}
}- Draw skeletons using
keypointsandskeletonarrays from metadata Pseudo code:
const skeleton = detection["skeleton"] || [];
for (let s = 0; s < skeleton.length; s++) {
const coords = detection["coords"] || [];
const [x0, y0] = coords[skeleton[s][0]];
const [x1, y1] = coords[skeleton[s][1]];
if (!(x0 == 0.0 && y0 == 0.0) && !(x1 == 0.0 && y1 == 0.0)) {
context.beginPath();
context.strokeStyle = colorsTeknoirBrand.horizon_yellow;
context.fillStyle = colorsTeknoirBrand.gold;
context.moveTo(x0 * canvas.width, y0 * canvas.height);
context.lineTo(x1 * canvas.width, y1 * canvas.height);
context.stroke();
context.closePath();
}
}
// POSE KEYPOINTS
function drawDot(x, y, color = 'blue', radius = 2) {
context.beginPath();
context.strokeStyle = colorsTeknoirBrand.horizon_yellow;
context.arc(x, y, radius, 0, Math.PI * 2);
context.fillStyle = colorsTeknoirBrand.gold;
context.fill();
context.closePath();
}
const coords = detection["coords"] || [];
//const keypoints = detection["keypoints"] || [];
for (let c = 0; c < coords.length; c++) {
const [x, y] = coords[c] || [0.0, 0.0];
if (!(x == 0.0 && y == 0.0)) {
const xc = x * canvas.width;
const yc = y * canvas.height;
drawDot(xc, yc);
//const label = keypoints[c] || "error";
//context.fillStyle = colorsTeknoirBrand.gold;
//context.fillText(label, xc, (yc + (fontSize - 2) ));
}
};Start with auto-reload:
npm run devStart normally:
npm startMongoDB Connection Error:
- Ensure MongoDB is running
- Check
MONGODB_URIin.envfile - Verify network connectivity
Images Not Loading:
- Check
MEDIASERVICE_URLin.envfile - Verify mediaservice is running and accessible
- Check CORS configuration if needed
No Alerts Showing:
- Verify MongoDB has data in the
historiandatabase,alertscollection - Check browser console for errors
- Verify API endpoints are responding
- Ensure documents have
type: "line_crossing"
MIT