diff --git a/backend/current_annotation_output/labels/1768759482305-frame_0001.txt b/backend/current_annotation_output/labels/1768759482305-frame_0001.txt
new file mode 100644
index 0000000..7485dee
--- /dev/null
+++ b/backend/current_annotation_output/labels/1768759482305-frame_0001.txt
@@ -0,0 +1,4 @@
+0 0.3895610272884369 0.6932320594787598 0.1280086189508438 0.1627638041973114
+0 0.16703420877456665 0.5642731785774231 0.09341136366128922 0.15167592465877533
+0 0.9560484290122986 0.9221291542053223 0.08790302276611328 0.15574170649051666
+0 0.13916879892349243 0.811154842376709 0.07705847173929214 0.1463843435049057
diff --git a/backend/current_annotation_output/labels/1768847750499-frame_0023.txt b/backend/current_annotation_output/labels/1768847750499-frame_0023.txt
new file mode 100644
index 0000000..fcbf597
--- /dev/null
+++ b/backend/current_annotation_output/labels/1768847750499-frame_0023.txt
@@ -0,0 +1,6 @@
+0 0.29829591512680054 0.6446008682250977 0.12156656384468079 0.15267311036586761
+0 0.49192899465560913 0.745903730392456 0.1493932455778122 0.18510030210018158
+0 0.7323252558708191 0.8575846552848816 0.1644657701253891 0.20728585124015808
+0 0.43945321440696716 0.26821741461753845 0.08059863746166229 0.10975001007318497
+0 0.14751005172729492 0.5243874788284302 0.07802576571702957 0.1305433213710785
+0 0.9607410430908203 0.9214945435523987 0.07851791381835938 0.15701085329055786
diff --git a/backend/current_annotation_output/visualized/1768759482305-frame_0001.jpg b/backend/current_annotation_output/visualized/1768759482305-frame_0001.jpg
new file mode 100644
index 0000000..d3064e8
Binary files /dev/null and b/backend/current_annotation_output/visualized/1768759482305-frame_0001.jpg differ
diff --git a/backend/current_annotation_output/visualized/1768847750499-frame_0023.jpg b/backend/current_annotation_output/visualized/1768847750499-frame_0023.jpg
new file mode 100644
index 0000000..103d2fa
Binary files /dev/null and b/backend/current_annotation_output/visualized/1768847750499-frame_0023.jpg differ
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 1e2ea6f..d143520 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -13,7 +13,9 @@
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
+ "multer": "^2.0.2",
"prisma": "^6.19.0",
+ "uuid": "^13.0.0",
"ws": "^8.18.3"
}
},
@@ -115,6 +117,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/append-field": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
+ "license": "MIT"
+ },
"node_modules/body-parser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
@@ -139,6 +147,23 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT"
+ },
+ "node_modules/busboy": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+ "dependencies": {
+ "streamsearch": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ }
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -241,6 +266,21 @@
"consola": "^3.2.3"
}
},
+ "node_modules/concat-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+ "engines": [
+ "node >= 6.0"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.0.2",
+ "typedarray": "^0.0.6"
+ }
+ },
"node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
@@ -791,12 +831,94 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
+ "node_modules/multer": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
+ "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
+ "license": "MIT",
+ "dependencies": {
+ "append-field": "^1.0.0",
+ "busboy": "^1.6.0",
+ "concat-stream": "^2.0.0",
+ "mkdirp": "^0.5.6",
+ "object-assign": "^4.1.1",
+ "type-is": "^1.6.18",
+ "xtend": "^4.0.2"
+ },
+ "engines": {
+ "node": ">= 10.16.0"
+ }
+ },
+ "node_modules/multer/node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
@@ -1024,6 +1146,20 @@
"destr": "^2.0.3"
}
},
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -1053,6 +1189,26 @@
"node": ">= 18"
}
},
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -1183,6 +1339,23 @@
"node": ">= 0.8"
}
},
+ "node_modules/streamsearch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
"node_modules/tinyexec": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
@@ -1215,6 +1388,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+ "license": "MIT"
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -1224,6 +1403,25 @@
"node": ">= 0.8"
}
},
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/uuid": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
+ "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist-node/bin/uuid"
+ }
+ },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -1259,6 +1457,15 @@
"optional": true
}
}
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
}
}
}
diff --git a/backend/package.json b/backend/package.json
index 98d9055..638a95e 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -4,6 +4,7 @@
"type": "module",
"main": "src/index.js",
"scripts": {
+ "start": "node src/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
@@ -15,7 +16,9 @@
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
+ "multer": "^2.0.2",
"prisma": "^6.19.0",
+ "uuid": "^13.0.0",
"ws": "^8.18.3"
}
}
diff --git a/backend/src/index.js b/backend/src/index.js
index 50a8e31..f531457 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -6,6 +6,10 @@ import { PrismaClient } from "@prisma/client";
import clientRoutes from "./routes/client.js";
import projectRoutes from "./routes/projects.js";
import cameraRoutes from "./routes/cameras.js";
+import streamRoutes from "./routes/streams.js";
+import annotationRoutes from "./routes/annotation.js";
+import path from "path";
+import { fileURLToPath } from "url";
// EXPRESS + PRISMA SETUP
const prisma = new PrismaClient();
@@ -17,6 +21,18 @@ app.use(express.json());
app.use("/clients", clientRoutes);
app.use("/projects", projectRoutes);
app.use("/cameras", cameraRoutes);
+app.use("/streams", streamRoutes);
+app.use("/api/annotation", annotationRoutes);
+
+// SERVE STATIC FILES
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const BACKEND_ROOT = path.join(__dirname, "../");
+
+// Serve uploads so frontend can view them
+app.use("/uploads", express.static(path.join(BACKEND_ROOT, "uploads")));
+// Serve results
+app.use("/results", express.static(path.join(BACKEND_ROOT, "current_annotation_output")));
app.get("/", (req, res) => {
res.json({ message: "Backend running" });
@@ -39,26 +55,23 @@ wss.on("connection", (ws) => {
console.log("Client connected to WebSocket");
ws.on("message", (msg) => {
- const text = msg.toString();
-
- // Check if message is valid base64 JPEG (Python frame)
- const isPythonFrame =
- text.length > 1000 && // base64 frame is large
- (text.startsWith("/") || text.startsWith("iVB") || text.startsWith("/9j"));
-
- if (!isPythonFrame) {
- console.log("Ignoring non-frame message");
- return;
- }
-
- console.log("Broadcasting frame:", text.length);
-
- // Broadcast only Python frames to all React clients
- wss.clients.forEach((client) => {
- if (client.readyState === WebSocket.OPEN) {
- client.send(text);
+ try {
+ const parsed = JSON.parse(msg);
+
+ // If it has streamId, it's a frame from Python
+ if (parsed.streamId && parsed.image) {
+ // Broadcast to all clients (Let frontend filter by streamId)
+ // Optimization: Could filter by client subscription if we implemented that
+ const broadcastMsg = JSON.stringify(parsed);
+ wss.clients.forEach((client) => {
+ if (client.readyState === WebSocket.OPEN) {
+ client.send(broadcastMsg);
+ }
+ });
}
- });
+ } catch (e) {
+ console.log("Received non-JSON message or invalid format");
+ }
});
});
@@ -66,4 +79,4 @@ wss.on("connection", (ws) => {
// =================================
// START EXPRESS API SERVER
// =================================
-app.listen(3000, () => console.log("Server running on port 3000"));
+app.listen(5000, () => console.log("Server running on port 5000"));
diff --git a/backend/src/routes/annotation.js b/backend/src/routes/annotation.js
new file mode 100644
index 0000000..380955d
--- /dev/null
+++ b/backend/src/routes/annotation.js
@@ -0,0 +1,145 @@
+import express from "express";
+import multer from "multer";
+import path from "path";
+import fs from "fs";
+import { spawn } from "child_process";
+import { fileURLToPath } from "url";
+
+const router = express.Router();
+
+// Get current directory dirname equivalent in ES modules
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// Define paths
+// Backend root is two levels up from src/routes (backend/src/routes -> backend)
+const BACKEND_ROOT = path.join(__dirname, "../../");
+const UPLOADS_DIR = path.join(BACKEND_ROOT, "uploads");
+const ANNOTATION_OUTPUT_DIR = path.join(BACKEND_ROOT, "current_annotation_output");
+
+// Ensure upload directory exists
+if (!fs.existsSync(UPLOADS_DIR)) {
+ fs.mkdirSync(UPLOADS_DIR, { recursive: true });
+}
+
+// Ensure output directory exists (cleanup logic might be needed per request in real app)
+if (!fs.existsSync(ANNOTATION_OUTPUT_DIR)) {
+ fs.mkdirSync(ANNOTATION_OUTPUT_DIR, { recursive: true });
+}
+
+// Multer storage
+const storage = multer.diskStorage({
+ destination: function (req, file, cb) {
+ cb(null, UPLOADS_DIR);
+ },
+ filename: function (req, file, cb) {
+ // Keep original name but sanitize or timestamp to avoid collisions if needed
+ // For now simple keep original for clarity in results
+ cb(null, Date.now() + "-" + file.originalname);
+ },
+});
+
+const upload = multer({ storage: storage });
+
+// POST /api/annotation/upload - Upload images
+router.post("/upload", upload.array("images", 50), (req, res) => {
+ try {
+ const files = req.files;
+ if (!files || files.length === 0) {
+ return res.status(400).json({ error: "No files uploaded" });
+ }
+
+ res.json({
+ message: `Uploaded ${files.length} images successfully`,
+ files: files.map(f => f.filename)
+ });
+ } catch (err) {
+ console.error("Upload error:", err);
+ res.status(500).json({ error: "Upload failed" });
+ }
+});
+
+// POST /api/annotation/process - Trigger python script
+router.post("/process", async (req, res) => {
+ const { confidence = 0.25 } = req.body;
+
+ // Python script path
+ // Assumes python-engine is sibling to backend
+ const PYTHON_ENGINE_DIR = path.join(BACKEND_ROOT, "../python-engine");
+ const SCRIPT_PATH = path.join(PYTHON_ENGINE_DIR, "auto_annotate.py");
+
+ console.log(`Starting annotation process...`);
+ console.log(`Input: ${UPLOADS_DIR}`);
+ console.log(`Output: ${ANNOTATION_OUTPUT_DIR}`);
+
+ // Clean output directory before running (optional, but good for demo to show only fresh results)
+ // For a multi-user app, we'd use session-based folders. staying simple for now.
+ try {
+ if (fs.existsSync(ANNOTATION_OUTPUT_DIR)) {
+ fs.rmSync(ANNOTATION_OUTPUT_DIR, { recursive: true, force: true });
+ fs.mkdirSync(ANNOTATION_OUTPUT_DIR);
+ }
+ } catch (e) {
+ console.error("Error cleaning output dir", e);
+ }
+
+ // Spawn python process
+ const pythonProcess = spawn("python", [
+ SCRIPT_PATH,
+ "--input", UPLOADS_DIR,
+ "--output", ANNOTATION_OUTPUT_DIR,
+ "--conf", str(confidence)
+ ]);
+
+ let scriptOutput = "";
+ let scriptError = "";
+
+ pythonProcess.stdout.on("data", (data) => {
+ console.log(`[Python]: ${data}`);
+ scriptOutput += data.toString();
+ });
+
+ pythonProcess.stderr.on("data", (data) => {
+ console.error(`[Python Err]: ${data}`);
+ scriptError += data.toString();
+ });
+
+ pythonProcess.on("close", (code) => {
+ console.log(`Python script exited with code ${code}`);
+
+ if (code !== 0) {
+ return res.status(500).json({ error: "Annotation script failed", details: scriptError });
+ }
+
+ // Read result files to send back to frontend
+ const visualizedDir = path.join(ANNOTATION_OUTPUT_DIR, "visualized");
+ const labelDir = path.join(ANNOTATION_OUTPUT_DIR, "labels");
+
+ let results = [];
+ if (fs.existsSync(visualizedDir)) {
+ const files = fs.readdirSync(visualizedDir);
+ results = files.map(f => ({
+ image: f,
+ label: f.replace(path.extname(f), ".txt")
+ }));
+ }
+
+ res.json({
+ message: "Annotation complete",
+ results: results
+ });
+ });
+});
+
+// GET /api/annotation/results - List results (optional, if we want to poll)
+router.get("/results", (req, res) => {
+ // implementation if needed
+ res.json({ message: "Not implemented yet, use process response" });
+});
+
+// Helper to convert explicit str cast (was typo in python args above)
+function str(val) {
+ return String(val);
+}
+
+export default router;
diff --git a/backend/src/routes/streams.js b/backend/src/routes/streams.js
new file mode 100644
index 0000000..e002889
--- /dev/null
+++ b/backend/src/routes/streams.js
@@ -0,0 +1,42 @@
+import express from "express";
+import { startStream, stopStream, getActiveStreams } from "../utils/engine.js";
+import { v4 as uuidv4 } from "uuid";
+
+const router = express.Router();
+
+// Start a new stream (Spawns Python Process)
+router.post("/start", (req, res) => {
+ let { source, existingStreamId } = req.body;
+
+ if (!source) {
+ return res.status(400).json({ error: "Source is required (RTSP URL or File Path)" });
+ }
+
+ // Hardcode demo video path
+ if (source === "demo") {
+ // Navigate from backend/src/routes to root/video/test_video.mp4
+ source = "D:\\Final-Year-Project\\baggage-platform\\video\\test_video.mp4";
+ }
+
+ // Use existing ID if provided (restarting a stream), or generate new
+ const streamId = existingStreamId || uuidv4();
+
+ startStream(streamId, source);
+
+ res.json({ success: true, streamId, message: "Stream started" });
+});
+
+// Stop a stream
+router.post("/stop", (req, res) => {
+ const { streamId } = req.body;
+ const stopped = stopStream(streamId);
+ res.json({ success: stopped, message: stopped ? "Stream stopped" : "Stream not found" });
+});
+
+// Get active streams
+router.get("/active", (req, res) => {
+ const activeIds = getActiveStreams();
+ res.json({ active: activeIds, count: activeIds.length });
+});
+
+export default router;
diff --git a/backend/src/utils/engine.js b/backend/src/utils/engine.js
new file mode 100644
index 0000000..fae9ba7
--- /dev/null
+++ b/backend/src/utils/engine.js
@@ -0,0 +1,58 @@
+import { spawn } from "child_process";
+import path from "path";
+
+// Keep track of active processes
+const activeStreams = new Map();
+
+export const startStream = (streamId, source) => {
+ if (activeStreams.has(streamId)) {
+ console.log(`Stream ${streamId} already running.`);
+ return;
+ }
+
+ console.log(`Starting Python Engine for ${streamId} with source: ${source}`);
+
+ // Path to python script (Adjust relative path as needed)
+ // Assuming backend is at D:\...\backend and python-engine is D:\...\python-engine
+ const pythonScript = path.resolve(process.cwd(), "../python-engine/live_yolo_engine.py");
+
+ const pythonProcess = spawn("python", [
+ pythonScript,
+ "--source", source,
+ "--streamId", streamId
+ ]);
+
+ pythonProcess.stdout.on("data", (data) => {
+ console.log(`[Python ${streamId}]: ${data}`);
+ });
+
+ pythonProcess.stderr.on("data", (data) => {
+ console.error(`[Python Err ${streamId}]: ${data}`);
+ });
+
+ pythonProcess.on("close", (code) => {
+ console.log(`[Python ${streamId}] exited with code ${code}`);
+ activeStreams.delete(streamId);
+ });
+
+ activeStreams.set(streamId, pythonProcess);
+};
+
+export const stopStream = (streamId) => {
+ const process = activeStreams.get(streamId);
+ if (process) {
+ process.kill();
+ activeStreams.delete(streamId);
+ console.log(`Stopped stream ${streamId}`);
+ return true;
+ }
+ return false;
+};
+
+export const getActiveStreams = () => {
+ return Array.from(activeStreams.keys());
+};
+
+export const isStreamActive = (streamId) => {
+ return activeStreams.has(streamId);
+};
diff --git a/backend/uploads/1768759482305-frame_0001.jpg b/backend/uploads/1768759482305-frame_0001.jpg
new file mode 100644
index 0000000..0afdca9
Binary files /dev/null and b/backend/uploads/1768759482305-frame_0001.jpg differ
diff --git a/backend/uploads/1768847750499-frame_0023.jpg b/backend/uploads/1768847750499-frame_0023.jpg
new file mode 100644
index 0000000..08189c2
Binary files /dev/null and b/backend/uploads/1768847750499-frame_0023.jpg differ
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 5c72589..dc8e356 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -17,10 +17,12 @@
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.13.2",
+ "lucide-react": "^0.562.0",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-router-dom": "^7.10.1",
"react-scripts": "5.0.1",
+ "recharts": "^3.6.0",
"web-vitals": "^2.1.4"
}
},
@@ -3394,6 +3396,42 @@
"url": "https://opencollective.com/popperjs"
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
+ "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^11.0.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "11.1.3",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz",
+ "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -3509,6 +3547,18 @@
"@sinonjs/commons": "^1.7.0"
}
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
"node_modules/@surma/rollup-plugin-off-main-thread": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
@@ -3941,6 +3991,69 @@
"@types/node": "*"
}
},
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
"node_modules/@types/eslint": {
"version": "8.56.12",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
@@ -4229,6 +4342,12 @@
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@@ -6767,6 +6886,127 @@
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -6861,6 +7101,12 @@
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
"node_modules/dedent": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
@@ -7509,6 +7755,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/es-toolkit": {
+ "version": "1.44.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz",
+ "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -9664,6 +9920,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/ipaddr.js": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
@@ -11616,6 +11881,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "0.562.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz",
+ "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@@ -14223,6 +14497,29 @@
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"license": "MIT"
},
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -14407,6 +14704,52 @@
"node": ">=8.10.0"
}
},
+ "node_modules/recharts": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz",
+ "integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==",
+ "license": "MIT",
+ "workspaces": [
+ "www"
+ ],
+ "dependencies": {
+ "@reduxjs/toolkit": "1.x.x || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^10.1.1",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.1.1",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/recharts/node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "license": "MIT"
+ },
+ "node_modules/recharts/node_modules/immer": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/recursive-readdir": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz",
@@ -14432,6 +14775,21 @@
"node": ">=8"
}
},
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -14585,6 +14943,12 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -16535,6 +16899,12 @@
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
"license": "MIT"
},
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -17023,6 +17393,15 @@
"requires-port": "^1.0.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -17097,6 +17476,28 @@
"node": ">= 0.8"
}
},
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
"node_modules/w3c-hr-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 4ed5ddf..8ccf30d 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,10 +12,12 @@
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.13.2",
+ "lucide-react": "^0.562.0",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-router-dom": "^7.10.1",
"react-scripts": "5.0.1",
+ "recharts": "^3.6.0",
"web-vitals": "^2.1.4"
},
"scripts": {
diff --git a/frontend/src/App.js b/frontend/src/App.js
index f2189b7..0a4658a 100644
--- a/frontend/src/App.js
+++ b/frontend/src/App.js
@@ -5,6 +5,10 @@ import ClientsPage from "./pages/ClientsPage";
import ProjectsPage from "./pages/ProjectsPage";
import CamerasPage from "./pages/CamerasPage";
import LiveCameraPage from "./pages/LiveCameraPage";
+import DashboardHostPage from "./pages/DashboardHostPage";
+import SingleFeedDashboard from "./pages/SingleFeedDashboard";
+import AutoAnnotationPage from "./pages/AutoAnnotationPage";
+import SettingsPage from "./pages/SettingsPage";
function App() {
@@ -16,6 +20,10 @@ function App() {
} />
} />
} />
+ } />
+ } />
+ } />
+ } />
@@ -23,3 +31,4 @@ function App() {
}
export default App;
+
diff --git a/frontend/src/components/StreamViewer.css b/frontend/src/components/StreamViewer.css
new file mode 100644
index 0000000..712fc1a
--- /dev/null
+++ b/frontend/src/components/StreamViewer.css
@@ -0,0 +1,250 @@
+/* ===================================
+ STREAM VIEWER COMPONENT STYLES
+ =================================== */
+
+.stream-viewer {
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ transition: all var(--transition-normal);
+}
+
+.stream-viewer:hover {
+ border-color: rgba(99, 102, 241, 0.3);
+ box-shadow: var(--shadow-md);
+}
+
+/* Stream Header */
+.stream-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 14px 18px;
+ background: var(--color-bg-tertiary);
+ border-bottom: 1px solid var(--color-border);
+}
+
+.stream-info {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.stream-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+ max-width: 200px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.stream-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 10px;
+ font-weight: 700;
+ padding: 4px 8px;
+ border-radius: 4px;
+ text-transform: uppercase;
+}
+
+.stream-badge.live {
+ background: rgba(16, 185, 129, 0.15);
+ color: var(--color-success);
+}
+
+.stream-badge.live::before {
+ content: '';
+ width: 6px;
+ height: 6px;
+ background: var(--color-success);
+ border-radius: 50%;
+ animation: pulse 2s infinite;
+}
+
+.stream-badge.connecting {
+ background: rgba(245, 158, 11, 0.15);
+ color: var(--color-warning);
+}
+
+.stream-badge.offline {
+ background: rgba(239, 68, 68, 0.15);
+ color: var(--color-danger);
+}
+
+/* Stream Stats */
+.stream-stats {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.stat-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ color: var(--color-text-muted);
+}
+
+.stat-item strong {
+ color: var(--color-text-primary);
+ font-weight: 600;
+}
+
+/* Video Area */
+.stream-video {
+ position: relative;
+ aspect-ratio: 16 / 9;
+ background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
+ overflow: hidden;
+}
+
+.stream-video img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+}
+
+.stream-placeholder {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 16px;
+ color: #64748b;
+}
+
+.stream-placeholder span {
+ font-size: 13px;
+}
+
+/* Count Badge Overlay */
+.count-badge {
+ position: absolute;
+ bottom: 16px;
+ right: 16px;
+ background: rgba(255, 255, 255, 0.95);
+ backdrop-filter: blur(8px);
+ padding: 10px 16px;
+ border-radius: 10px;
+ text-align: center;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.count-badge .label {
+ font-size: 9px;
+ font-weight: 700;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.count-badge .value {
+ font-size: 28px;
+ font-weight: 800;
+ color: var(--color-text-primary);
+ line-height: 1;
+}
+
+/* Stream Footer */
+.stream-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 18px;
+ border-top: 1px solid var(--color-border);
+}
+
+.stream-meta {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.meta-label {
+ font-size: 11px;
+ color: var(--color-text-muted);
+}
+
+.meta-value {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--color-accent-primary);
+}
+
+.stream-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.action-btn {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--color-bg-tertiary);
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ color: var(--color-text-muted);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.action-btn:hover {
+ background: var(--color-bg-secondary);
+ color: var(--color-text-primary);
+ border-color: var(--color-accent-primary);
+}
+
+.action-btn.danger:hover {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--color-danger);
+ border-color: var(--color-danger);
+}
+
+/* FPS Indicator */
+.fps-indicator {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ background: rgba(0, 0, 0, 0.7);
+ backdrop-filter: blur(4px);
+ padding: 4px 10px;
+ border-radius: 4px;
+ font-size: 11px;
+ font-weight: 600;
+ color: white;
+}
+
+.fps-indicator.good {
+ color: var(--color-success);
+}
+
+.fps-indicator.warning {
+ color: var(--color-warning);
+}
+
+.fps-indicator.bad {
+ color: var(--color-danger);
+}
+
+/* Responsive */
+@media (max-width: 600px) {
+ .stream-header {
+ flex-direction: column;
+ gap: 8px;
+ align-items: flex-start;
+ }
+
+ .stream-stats {
+ width: 100%;
+ justify-content: space-between;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/components/StreamViewer.jsx b/frontend/src/components/StreamViewer.jsx
new file mode 100644
index 0000000..13a0a66
--- /dev/null
+++ b/frontend/src/components/StreamViewer.jsx
@@ -0,0 +1,117 @@
+import React, { useEffect, useState, useRef } from "react";
+import "./StreamViewer.css";
+
+const StreamViewer = ({ streamId, source, onStop }) => {
+ const [imageSrc, setImageSrc] = useState(null);
+ const [status, setStatus] = useState("connecting");
+ const [fps, setFps] = useState(0);
+ const [frameCount, setFrameCount] = useState(0);
+ const ws = useRef(null);
+ const frameTimestamps = useRef([]);
+
+ useEffect(() => {
+ ws.current = new WebSocket("ws://localhost:8081");
+
+ ws.current.onopen = () => {
+ setStatus("live");
+ };
+
+ ws.current.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data);
+ if (data.streamId === streamId) {
+ setImageSrc(`data:image/jpeg;base64,${data.image}`);
+ setFrameCount(prev => prev + 1);
+
+ // Calculate FPS
+ const now = Date.now();
+ frameTimestamps.current.push(now);
+ frameTimestamps.current = frameTimestamps.current.filter(t => now - t < 1000);
+ setFps(frameTimestamps.current.length);
+ }
+ } catch (err) {
+ console.error("WS Parse Error", err);
+ }
+ };
+
+ ws.current.onerror = () => {
+ setStatus("error");
+ };
+
+ ws.current.onclose = () => {
+ setStatus("disconnected");
+ };
+
+ return () => {
+ if (ws.current) {
+ ws.current.close();
+ }
+ };
+ }, [streamId]);
+
+ const getStatusBadge = () => {
+ const statusConfig = {
+ live: { class: "badge-live", icon: "🟢", text: "LIVE" },
+ connecting: { class: "badge-connecting", icon: "🟡", text: "CONNECTING" },
+ disconnected: { class: "badge-offline", icon: "🔴", text: "OFFLINE" },
+ error: { class: "badge-offline", icon: "⚠️", text: "ERROR" }
+ };
+ const config = statusConfig[status] || statusConfig.connecting;
+ return (
+
+ {config.icon} {config.text}
+
+ );
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ Stream #{streamId.slice(0, 8)}
+ {getStatusBadge()}
+
+
+
+
+ {/* Video Feed */}
+
+ {imageSrc ? (
+

+ ) : (
+
+
+
Initializing YOLO model...
+
+ )}
+
+ {/* Overlay Stats */}
+ {imageSrc && (
+
+
+ {fps}
+ FPS
+
+
+ {frameCount}
+ Frames
+
+
+ )}
+
+
+ {/* Footer */}
+
+
+ );
+};
+
+export default StreamViewer;
diff --git a/frontend/src/components/TopNavbar.css b/frontend/src/components/TopNavbar.css
new file mode 100644
index 0000000..48802ba
--- /dev/null
+++ b/frontend/src/components/TopNavbar.css
@@ -0,0 +1,267 @@
+/* ===================================
+ TOP NAVBAR STYLES
+ =================================== */
+
+.top-navbar {
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ background: var(--color-bg-secondary);
+ border-bottom: 1px solid var(--color-border);
+ padding: 0 24px;
+ height: 64px;
+ display: flex;
+ align-items: center;
+}
+
+.navbar-container {
+ width: 100%;
+ max-width: 1600px;
+ margin: 0 auto;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 32px;
+}
+
+/* Brand Section */
+.navbar-brand {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-shrink: 0;
+}
+
+.brand-icon {
+ width: 40px;
+ height: 40px;
+ background: var(--color-accent-gradient);
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+}
+
+.brand-text {
+ display: flex;
+ flex-direction: column;
+}
+
+.brand-name {
+ font-size: 16px;
+ font-weight: 700;
+ color: var(--color-text-primary);
+ line-height: 1.2;
+}
+
+.brand-subtitle {
+ font-size: 11px;
+ color: var(--color-text-muted);
+}
+
+/* Navigation Links */
+.navbar-nav {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.nav-link {
+ padding: 8px 16px;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--color-text-secondary);
+ text-decoration: none;
+ border-radius: 8px;
+ transition: all var(--transition-fast);
+}
+
+.nav-link:hover {
+ color: var(--color-text-primary);
+ background: var(--color-bg-tertiary);
+}
+
+.nav-link.active {
+ color: var(--color-accent-primary);
+ background: rgba(99, 102, 241, 0.1);
+ font-weight: 600;
+}
+
+/* Actions Section */
+.navbar-actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-shrink: 0;
+}
+
+/* Camera Selector */
+.camera-selector {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 14px;
+ background: var(--color-bg-primary);
+ border: 1px solid var(--color-border);
+ border-radius: 8px;
+ font-size: 13px;
+ color: var(--color-text-primary);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.camera-selector:hover {
+ border-color: var(--color-accent-primary);
+}
+
+.dropdown-menu {
+ position: absolute;
+ top: calc(100% + 8px);
+ left: 0;
+ min-width: 200px;
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: 10px;
+ box-shadow: var(--shadow-lg);
+ overflow: hidden;
+ z-index: 50;
+}
+
+.dropdown-item {
+ padding: 10px 14px;
+ font-size: 13px;
+ color: var(--color-text-secondary);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.dropdown-item:hover {
+ background: var(--color-bg-tertiary);
+ color: var(--color-text-primary);
+}
+
+.dropdown-item.active {
+ background: rgba(99, 102, 241, 0.1);
+ color: var(--color-accent-primary);
+}
+
+/* Date Picker */
+.date-picker {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 14px;
+ background: var(--color-bg-primary);
+ border: 1px solid var(--color-border);
+ border-radius: 8px;
+ font-size: 13px;
+ color: var(--color-text-primary);
+ cursor: pointer;
+}
+
+/* Icon Buttons */
+.icon-btn {
+ width: 38px;
+ height: 38px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: 1px solid var(--color-border);
+ border-radius: 8px;
+ color: var(--color-text-secondary);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.icon-btn:hover {
+ background: var(--color-bg-tertiary);
+ color: var(--color-text-primary);
+ border-color: var(--color-accent-primary);
+}
+
+.notification-btn {
+ position: relative;
+}
+
+.notification-badge {
+ position: absolute;
+ top: -4px;
+ right: -4px;
+ min-width: 18px;
+ height: 18px;
+ background: var(--color-danger);
+ color: white;
+ font-size: 10px;
+ font-weight: 700;
+ border-radius: 9px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 4px;
+}
+
+/* Export Button */
+.export-btn {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 18px;
+ background: var(--color-accent-gradient);
+ border: none;
+ border-radius: 8px;
+ color: white;
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all var(--transition-normal);
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
+}
+
+.export-btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
+}
+
+/* User Avatar */
+.user-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 10px;
+ overflow: hidden;
+ border: 2px solid var(--color-accent-primary);
+ cursor: pointer;
+}
+
+.user-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+/* Responsive */
+@media (max-width: 1200px) {
+
+ .camera-selector,
+ .date-picker {
+ display: none;
+ }
+}
+
+@media (max-width: 900px) {
+ .brand-subtitle {
+ display: none;
+ }
+
+ .export-btn span {
+ display: none;
+ }
+}
+
+@media (max-width: 768px) {
+ .navbar-nav {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/components/TopNavbar.jsx b/frontend/src/components/TopNavbar.jsx
new file mode 100644
index 0000000..0c2062c
--- /dev/null
+++ b/frontend/src/components/TopNavbar.jsx
@@ -0,0 +1,113 @@
+import React, { useState } from "react";
+import { Link, useLocation } from "react-router-dom";
+import {
+ Bell, Moon, Sun, Search, Calendar, ChevronDown,
+ LayoutDashboard, Camera, AlertTriangle, Settings, FileText
+} from "lucide-react";
+import "./TopNavbar.css";
+
+const TopNavbar = () => {
+ const location = useLocation();
+ const [isDarkMode, setIsDarkMode] = useState(false);
+ const [showCameraDropdown, setShowCameraDropdown] = useState(false);
+
+ const isActive = (path) => location.pathname.startsWith(path);
+
+ const navLinks = [
+ { path: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
+ { path: "/cameras", label: "Cameras", icon: Camera },
+ { path: "/auto-annotation", label: "Auto-Annotate", icon: FileText },
+ { path: "/settings", label: "Settings", icon: Settings },
+ ];
+
+ const today = new Date().toLocaleDateString('en-GB', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric'
+ });
+
+ return (
+
+ );
+};
+
+export default TopNavbar;
diff --git a/frontend/src/index.css b/frontend/src/index.css
index ec2585e..610285c 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1,13 +1,827 @@
+/* ===================================
+ BAGGAGE AI - GLOBAL STYLES
+ Modern Light Theme Design System
+ =================================== */
+
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
+
+:root {
+ /* Color Palette - Light Theme */
+ --color-bg-primary: #f8fafc;
+ --color-bg-secondary: #ffffff;
+ --color-bg-tertiary: #f1f5f9;
+ --color-bg-card: #ffffff;
+
+ --color-accent-primary: #6366f1;
+ --color-accent-secondary: #8b5cf6;
+ --color-accent-gradient: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
+
+ --color-text-primary: #1e293b;
+ --color-text-secondary: #475569;
+ --color-text-muted: #94a3b8;
+
+ --color-success: #10b981;
+ --color-warning: #f59e0b;
+ --color-danger: #ef4444;
+ --color-info: #3b82f6;
+
+ --color-border: #e2e8f0;
+ --color-glow: rgba(99, 102, 241, 0.2);
+
+ /* Shadows */
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
+ --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.1);
+ --shadow-glow: 0 0 20px var(--color-glow);
+
+ /* Transitions */
+ --transition-fast: 150ms ease;
+ --transition-normal: 250ms ease;
+ --transition-slow: 400ms ease;
+
+ /* Border Radius */
+ --radius-sm: 6px;
+ --radius-md: 10px;
+ --radius-lg: 16px;
+ --radius-xl: 24px;
+}
+
+* {
+ box-sizing: border-box;
+}
+
body {
margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ background: var(--color-bg-primary);
+ color: var(--color-text-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
+ line-height: 1.6;
+}
+
+/* Scrollbar Styling */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--color-bg-tertiary);
+}
+
+::-webkit-scrollbar-thumb {
+ background: #cbd5e1;
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--color-accent-primary);
+}
+
+/* ===================================
+ UTILITY CLASSES
+ =================================== */
+
+.glass {
+ background: rgba(255, 255, 255, 0.9);
+ backdrop-filter: blur(12px);
+ border: 1px solid var(--color-border);
+}
+
+.gradient-text {
+ background: var(--color-accent-gradient);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.glow {
+ box-shadow: var(--shadow-glow);
+}
+
+/* ===================================
+ BUTTON STYLES
+ =================================== */
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 12px 24px;
+ border: none;
+ border-radius: var(--radius-md);
+ font-family: inherit;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all var(--transition-normal);
+ text-decoration: none;
+}
+
+.btn-primary {
+ background: var(--color-accent-gradient);
+ color: white;
+ box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35);
+}
+
+.btn-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(99, 102, 241, 0.45);
+}
+
+.btn-primary:active {
+ transform: translateY(0);
+}
+
+.btn-primary:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none;
+}
+
+.btn-secondary {
+ background: var(--color-bg-tertiary);
+ color: var(--color-text-primary);
+ border: 1px solid var(--color-border);
+}
+
+.btn-secondary:hover {
+ background: var(--color-bg-primary);
+ border-color: var(--color-accent-primary);
+}
+
+.btn-danger {
+ background: var(--color-danger);
+ color: white;
+}
+
+.btn-danger:hover {
+ background: #dc2626;
+ transform: translateY(-1px);
+}
+
+.btn-ghost {
+ background: transparent;
+ color: var(--color-text-secondary);
+ border: 1px solid var(--color-border);
+}
+
+.btn-ghost:hover {
+ background: var(--color-bg-tertiary);
+ color: var(--color-text-primary);
+}
+
+.btn-sm {
+ padding: 8px 16px;
+ font-size: 13px;
+}
+
+/* ===================================
+ INPUT STYLES
+ =================================== */
+
+.input {
+ width: 100%;
+ padding: 14px 18px;
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ color: var(--color-text-primary);
+ font-family: inherit;
+ font-size: 14px;
+ transition: all var(--transition-normal);
+}
+
+.input::placeholder {
+ color: var(--color-text-muted);
+}
+
+.input:focus {
+ outline: none;
+ border-color: var(--color-accent-primary);
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
+}
+
+/* ===================================
+ CARD STYLES
+ =================================== */
+
+.card {
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ transition: all var(--transition-normal);
+ box-shadow: var(--shadow-sm);
}
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
+.card:hover {
+ border-color: rgba(99, 102, 241, 0.3);
+ box-shadow: var(--shadow-md);
+}
+
+.card-header {
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--color-border);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.card-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.card-body {
+ padding: 20px;
+}
+
+/* ===================================
+ BADGE STYLES
+ =================================== */
+
+.badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 10px;
+ border-radius: 9999px;
+ font-size: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.badge-live {
+ background: rgba(16, 185, 129, 0.15);
+ color: var(--color-success);
+ animation: pulse-badge 2s infinite;
+}
+
+.badge-live::before {
+ content: '';
+ width: 6px;
+ height: 6px;
+ background: var(--color-success);
+ border-radius: 50%;
+}
+
+.badge-offline {
+ background: rgba(239, 68, 68, 0.15);
+ color: var(--color-danger);
+}
+
+.badge-connecting {
+ background: rgba(245, 158, 11, 0.15);
+ color: var(--color-warning);
+}
+
+.badge-normal {
+ background: rgba(16, 185, 129, 0.1);
+ color: var(--color-success);
+}
+
+.badge-warning {
+ background: rgba(245, 158, 11, 0.1);
+ color: var(--color-warning);
+}
+
+.badge-excellent {
+ background: rgba(99, 102, 241, 0.1);
+ color: var(--color-accent-primary);
+}
+
+@keyframes pulse-badge {
+
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0.7;
+ }
+}
+
+/* ===================================
+ LAYOUT HELPERS
+ =================================== */
+
+.page-container {
+ padding: 24px 32px;
+ min-height: calc(100vh - 64px);
+}
+
+.page-title {
+ font-size: 24px;
+ font-weight: 700;
+ margin: 0 0 4px 0;
+ color: var(--color-text-primary);
+}
+
+.page-subtitle {
+ font-size: 14px;
+ color: var(--color-text-muted);
+ margin: 0 0 24px 0;
+}
+
+/* Grid Layout */
+.stream-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
+ gap: 24px;
+}
+
+@media (max-width: 768px) {
+ .stream-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .page-container {
+ padding: 16px;
+ }
+}
+
+/* ===================================
+ METRIC CARDS
+ =================================== */
+
+.metric-card {
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ padding: 20px;
+ transition: all var(--transition-normal);
+}
+
+.metric-card:hover {
+ box-shadow: var(--shadow-md);
+ border-color: rgba(99, 102, 241, 0.2);
+}
+
+.metric-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 12px;
+}
+
+.metric-label {
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--color-text-muted);
+}
+
+.metric-value {
+ font-size: 32px;
+ font-weight: 700;
+ color: var(--color-text-primary);
+ line-height: 1.1;
+}
+
+.metric-subtext {
+ font-size: 12px;
+ color: var(--color-text-muted);
+ margin-top: 4px;
+}
+
+.metric-trend {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 12px;
+ font-weight: 600;
+ margin-top: 8px;
+}
+
+.metric-trend.up {
+ color: var(--color-success);
+}
+
+.metric-trend.down {
+ color: var(--color-danger);
+}
+
+.metric-trend.warning {
+ color: var(--color-warning);
+}
+
+/* ===================================
+ ALERT BANNER
+ =================================== */
+
+.alert-banner {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 14px 20px;
+ border-radius: var(--radius-md);
+ margin-bottom: 24px;
}
+
+.alert-banner.warning {
+ background: rgba(245, 158, 11, 0.1);
+ border: 1px solid rgba(245, 158, 11, 0.3);
+}
+
+.alert-banner.danger {
+ background: rgba(239, 68, 68, 0.1);
+ border: 1px solid rgba(239, 68, 68, 0.3);
+}
+
+.alert-banner.success {
+ background: rgba(16, 185, 129, 0.1);
+ border: 1px solid rgba(16, 185, 129, 0.3);
+}
+
+.alert-icon {
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.alert-banner.warning .alert-icon {
+ background: rgba(245, 158, 11, 0.2);
+ color: var(--color-warning);
+}
+
+.alert-content h4 {
+ margin: 0;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+}
+
+.alert-content p {
+ margin: 2px 0 0;
+ font-size: 13px;
+ color: var(--color-text-secondary);
+}
+
+/* ===================================
+ PROGRESS BARS
+ =================================== */
+
+.progress-bar {
+ height: 6px;
+ background: var(--color-bg-tertiary);
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.progress-fill {
+ height: 100%;
+ background: var(--color-accent-gradient);
+ border-radius: 3px;
+ transition: width 0.5s ease;
+}
+
+.progress-fill.success {
+ background: var(--color-success);
+}
+
+.progress-fill.warning {
+ background: var(--color-warning);
+}
+
+/* ===================================
+ ANIMATIONS
+ =================================== */
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateX(-20px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+@keyframes scaleIn {
+ from {
+ opacity: 0;
+ transform: scale(0.95);
+ }
+
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.animate-fadeIn {
+ animation: fadeIn 0.4s ease forwards;
+}
+
+.animate-slideIn {
+ animation: slideIn 0.3s ease forwards;
+}
+
+.animate-scaleIn {
+ animation: scaleIn 0.3s ease forwards;
+}
+
+/* Stagger children animations */
+.stagger-children>* {
+ animation: fadeIn 0.4s ease forwards;
+ opacity: 0;
+}
+
+.stagger-children>*:nth-child(1) {
+ animation-delay: 0.05s;
+}
+
+.stagger-children>*:nth-child(2) {
+ animation-delay: 0.1s;
+}
+
+.stagger-children>*:nth-child(3) {
+ animation-delay: 0.15s;
+}
+
+.stagger-children>*:nth-child(4) {
+ animation-delay: 0.2s;
+}
+
+.stagger-children>*:nth-child(5) {
+ animation-delay: 0.25s;
+}
+
+.stagger-children>*:nth-child(6) {
+ animation-delay: 0.3s;
+}
+
+/* ===================================
+ EMPTY STATE
+ =================================== */
+
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 64px 32px;
+ text-align: center;
+ background: var(--color-bg-secondary);
+ border: 2px dashed var(--color-border);
+ border-radius: var(--radius-lg);
+}
+
+.empty-state-icon {
+ font-size: 48px;
+ margin-bottom: 16px;
+ opacity: 0.5;
+}
+
+.empty-state-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+ margin-bottom: 8px;
+}
+
+.empty-state-text {
+ font-size: 14px;
+ color: var(--color-text-muted);
+ max-width: 300px;
+}
+
+/* ===================================
+ LOADING STATES
+ =================================== */
+
+.skeleton {
+ background: linear-gradient(90deg,
+ var(--color-bg-tertiary) 25%,
+ var(--color-bg-secondary) 50%,
+ var(--color-bg-tertiary) 75%);
+ background-size: 200% 100%;
+ animation: skeleton-loading 1.5s infinite;
+ border-radius: var(--radius-sm);
+}
+
+@keyframes skeleton-loading {
+ 0% {
+ background-position: 200% 0;
+ }
+
+ 100% {
+ background-position: -200% 0;
+ }
+}
+
+.spinner {
+ width: 24px;
+ height: 24px;
+ border: 3px solid var(--color-border);
+ border-top-color: var(--color-accent-primary);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+/* ===================================
+ SECTION HEADERS
+ =================================== */
+
+.section-header {
+ font-size: 11px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.8px;
+ color: var(--color-text-muted);
+ margin-bottom: 12px;
+}
+
+/* ===================================
+ PERFORMANCE ITEMS
+ =================================== */
+
+.perf-item {
+ padding: 16px 0;
+ border-bottom: 1px solid var(--color-border);
+}
+
+.perf-item:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+}
+
+.perf-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 8px;
+}
+
+.perf-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ color: var(--color-text-muted);
+}
+
+.perf-badge {
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+ color: var(--color-success);
+}
+
+.perf-value {
+ display: flex;
+ align-items: baseline;
+ gap: 4px;
+}
+
+.perf-value .number {
+ font-size: 24px;
+ font-weight: 700;
+ color: var(--color-text-primary);
+}
+
+.perf-value .unit {
+ font-size: 13px;
+ color: var(--color-text-muted);
+}
+
+/* ===================================
+ CAMERA CARDS
+ =================================== */
+
+.camera-card {
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ transition: all var(--transition-normal);
+}
+
+.camera-card:hover {
+ box-shadow: var(--shadow-md);
+ transform: translateY(-2px);
+}
+
+.camera-preview {
+ position: relative;
+ aspect-ratio: 16/10;
+ background: #1e293b;
+ overflow: hidden;
+}
+
+.camera-preview img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.camera-status-badge {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+}
+
+.camera-count-badge {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ background: rgba(0, 0, 0, 0.7);
+ backdrop-filter: blur(8px);
+ padding: 6px 12px;
+ border-radius: 8px;
+ color: white;
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.camera-info {
+ padding: 16px;
+}
+
+.camera-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+ margin-bottom: 4px;
+}
+
+.camera-location {
+ font-size: 12px;
+ color: var(--color-text-muted);
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.camera-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+ border-top: 1px solid var(--color-border);
+ background: var(--color-bg-tertiary);
+}
+
+.camera-tags {
+ display: flex;
+ gap: 8px;
+}
+
+.camera-tag {
+ font-size: 11px;
+ font-weight: 600;
+ padding: 4px 8px;
+ border-radius: 4px;
+}
+
+.camera-tag.normal {
+ background: rgba(16, 185, 129, 0.1);
+ color: var(--color-success);
+}
+
+.camera-tag.detection {
+ background: rgba(99, 102, 241, 0.1);
+ color: var(--color-accent-primary);
+}
+
+.camera-tag.high-volume {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--color-danger);
+}
+
+.camera-tag.maintenance {
+ background: rgba(245, 158, 11, 0.1);
+ color: var(--color-warning);
+}
\ No newline at end of file
diff --git a/frontend/src/layout/SidebarLayout.jsx b/frontend/src/layout/SidebarLayout.jsx
index 77178d4..d91e873 100644
--- a/frontend/src/layout/SidebarLayout.jsx
+++ b/frontend/src/layout/SidebarLayout.jsx
@@ -1,22 +1,26 @@
-import { Link } from "react-router-dom";
-import "./sidebar.css";
+import TopNavbar from "../components/TopNavbar";
+import "./navbar-layout.css";
export default function SidebarLayout({ children }) {
return (
-
- {/* Sidebar */}
-
-
Baggage AI
- Clients
- Projects
- Cameras
- Live Stream
-
-
- {/* Main Content */}
-
);
}
diff --git a/frontend/src/layout/navbar-layout.css b/frontend/src/layout/navbar-layout.css
new file mode 100644
index 0000000..a9103b5
--- /dev/null
+++ b/frontend/src/layout/navbar-layout.css
@@ -0,0 +1,93 @@
+/* ===================================
+ NAVBAR LAYOUT STYLES
+ =================================== */
+
+.app-layout {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background: var(--color-bg-primary);
+}
+
+.main-content {
+ flex: 1;
+ overflow: auto;
+}
+
+/* Footer */
+.app-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 32px;
+ background: var(--color-bg-secondary);
+ border-top: 1px solid var(--color-border);
+ font-size: 12px;
+ color: var(--color-text-muted);
+}
+
+.footer-left span {
+ color: var(--color-text-muted);
+}
+
+.footer-right {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+}
+
+.footer-status {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--color-success);
+ font-weight: 500;
+}
+
+.status-dot {
+ width: 8px;
+ height: 8px;
+ background: var(--color-success);
+ border-radius: 50%;
+ animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+
+ 0%,
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+
+ 50% {
+ opacity: 0.6;
+ transform: scale(0.9);
+ }
+}
+
+.footer-link {
+ color: var(--color-accent-primary);
+ text-decoration: none;
+ font-weight: 500;
+ transition: color var(--transition-fast);
+}
+
+.footer-link:hover {
+ color: var(--color-accent-secondary);
+ text-decoration: underline;
+}
+
+@media (max-width: 768px) {
+ .app-footer {
+ flex-direction: column;
+ gap: 12px;
+ text-align: center;
+ padding: 16px;
+ }
+
+ .footer-right {
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/layout/sidebar.css b/frontend/src/layout/sidebar.css
index f95ea08..8b5d014 100644
--- a/frontend/src/layout/sidebar.css
+++ b/frontend/src/layout/sidebar.css
@@ -1,27 +1,197 @@
+/* ===================================
+ BAGGAGE AI - SIDEBAR STYLES
+ Premium Navigation Design
+ =================================== */
+
.sidebar {
- width: 220px;
- height: 100vh;
- background: #1e1e2f;
- color: white;
- padding: 20px;
- display: flex;
- flex-direction: column;
+ width: 260px;
+ min-height: 100vh;
+ background: linear-gradient(180deg, #1a1a2e 0%, #16162a 100%);
+ border-right: 1px solid rgba(255, 255, 255, 0.06);
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ position: sticky;
+ top: 0;
+}
+
+/* Logo Section */
+.sidebar-header {
+ padding: 28px 24px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
+}
+
+.sidebar-logo {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.sidebar-logo-icon {
+ width: 42px;
+ height: 42px;
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+ box-shadow: 0 4px 14px rgba(99, 102, 241, 0.4);
+}
+
+.sidebar-logo-text {
+ font-size: 20px;
+ font-weight: 700;
+ color: #f1f5f9;
+ letter-spacing: -0.5px;
+}
+
+.sidebar-logo-badge {
+ font-size: 10px;
+ font-weight: 600;
+ color: #8b5cf6;
+ background: rgba(139, 92, 246, 0.15);
+ padding: 2px 8px;
+ border-radius: 4px;
+ margin-left: auto;
+}
+
+/* Navigation */
+.sidebar-nav {
+ flex: 1;
+ padding: 20px 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.sidebar-section-title {
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ color: #64748b;
+ padding: 16px 12px 8px;
+ margin-top: 8px;
+}
+
+.sidebar-section-title:first-child {
+ margin-top: 0;
+}
+
+.sidebar-link {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ color: #94a3b8;
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: 500;
+ border-radius: 10px;
+ transition: all 200ms ease;
+ position: relative;
+}
+
+.sidebar-link:hover {
+ background: rgba(99, 102, 241, 0.1);
+ color: #f1f5f9;
+}
+
+.sidebar-link.active {
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.2) 0%, rgba(139, 92, 246, 0.15) 100%);
+ color: #f1f5f9;
+}
+
+.sidebar-link.active::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 3px;
+ height: 24px;
+ background: linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%);
+ border-radius: 0 3px 3px 0;
+}
+
+.sidebar-link-icon {
+ font-size: 18px;
+ width: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.sidebar-link-badge {
+ margin-left: auto;
+ font-size: 11px;
+ font-weight: 600;
+ padding: 2px 8px;
+ border-radius: 6px;
+ background: rgba(99, 102, 241, 0.2);
+ color: #a5b4fc;
+}
+
+.sidebar-link-badge.new {
+ background: rgba(16, 185, 129, 0.2);
+ color: #10b981;
+}
+
+/* Footer */
+.sidebar-footer {
+ padding: 20px 16px;
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
+}
+
+.sidebar-footer-content {
+ background: rgba(99, 102, 241, 0.1);
+ border: 1px solid rgba(99, 102, 241, 0.2);
+ border-radius: 12px;
+ padding: 16px;
+ text-align: center;
+}
+
+.sidebar-footer-title {
+ font-size: 13px;
+ font-weight: 600;
+ color: #f1f5f9;
+ margin-bottom: 4px;
+}
+
+.sidebar-footer-text {
+ font-size: 12px;
+ color: #94a3b8;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .sidebar {
+ width: 72px;
+ padding: 0;
}
-
- .sidebar h2 {
- margin-top: 0;
+
+ .sidebar-logo-text,
+ .sidebar-logo-badge,
+ .sidebar-section-title,
+ .sidebar-link span,
+ .sidebar-link-badge,
+ .sidebar-footer-content {
+ display: none;
}
-
- .sidebar a {
- color: white;
- text-decoration: none;
- padding: 10px 0;
- margin: 5px 0;
- display: block;
- border-radius: 4px;
+
+ .sidebar-header {
+ padding: 16px;
+ justify-content: center;
}
-
- .sidebar a:hover {
- background: #33334d;
+
+ .sidebar-link {
+ justify-content: center;
+ padding: 14px;
}
-
\ No newline at end of file
+
+ .sidebar-link-icon {
+ width: auto;
+ font-size: 20px;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/pages/AutoAnnotationPage.css b/frontend/src/pages/AutoAnnotationPage.css
new file mode 100644
index 0000000..91e44e5
--- /dev/null
+++ b/frontend/src/pages/AutoAnnotationPage.css
@@ -0,0 +1,553 @@
+/* ===================================
+ AUTO-ANNOTATION PAGE STYLES
+ =================================== */
+
+.auto-annotation-page {
+ max-width: 1400px;
+ margin: 0 auto;
+}
+
+.annotation-layout {
+ display: grid;
+ grid-template-columns: 400px 1fr;
+ gap: 24px;
+}
+
+/* Upload Section */
+.upload-section {
+ display: flex;
+ flex-direction: column;
+}
+
+.section-content {
+ padding: 24px;
+ flex: 1;
+}
+
+.section-title {
+ font-size: 22px;
+ font-weight: 700;
+ color: var(--color-text-primary);
+ margin: 0 0 8px 0;
+}
+
+.section-description {
+ font-size: 14px;
+ color: var(--color-text-muted);
+ margin: 0 0 24px 0;
+ line-height: 1.5;
+}
+
+/* Drop Zone */
+.drop-zone {
+ border: 2px dashed var(--color-border);
+ border-radius: var(--radius-lg);
+ padding: 40px 24px;
+ text-align: center;
+ cursor: pointer;
+ transition: all var(--transition-normal);
+ background: var(--color-bg-tertiary);
+}
+
+.drop-zone:hover {
+ border-color: var(--color-accent-primary);
+ background: rgba(99, 102, 241, 0.05);
+}
+
+.drop-icon {
+ width: 64px;
+ height: 64px;
+ margin: 0 auto 16px;
+ background: rgba(99, 102, 241, 0.1);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--color-accent-primary);
+}
+
+.drop-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+ margin: 0 0 4px 0;
+}
+
+.drop-subtitle {
+ font-size: 13px;
+ color: var(--color-text-muted);
+ margin: 0 0 16px 0;
+}
+
+.file-types {
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+}
+
+.file-type {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--color-text-muted);
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ padding: 4px 12px;
+ border-radius: 20px;
+}
+
+.files-selected {
+ margin-top: 16px;
+ padding: 8px 16px;
+ background: rgba(16, 185, 129, 0.1);
+ color: var(--color-success);
+ border-radius: 20px;
+ font-size: 13px;
+ font-weight: 600;
+ display: inline-block;
+}
+
+.hidden {
+ display: none;
+}
+
+/* Confidence Control */
+.confidence-control {
+ margin: 24px 0;
+}
+
+.confidence-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+}
+
+.confidence-label {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--color-text-primary);
+}
+
+.confidence-value {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--color-accent-primary);
+}
+
+.slider-container {
+ position: relative;
+}
+
+.confidence-slider {
+ width: 100%;
+ height: 6px;
+ -webkit-appearance: none;
+ appearance: none;
+ background: var(--color-bg-tertiary);
+ border-radius: 3px;
+ outline: none;
+}
+
+.confidence-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 18px;
+ height: 18px;
+ background: var(--color-accent-primary);
+ border-radius: 50%;
+ cursor: pointer;
+ box-shadow: 0 2px 6px rgba(99, 102, 241, 0.4);
+}
+
+.slider-labels {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 8px;
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+}
+
+/* Start Button */
+.start-btn {
+ width: 100%;
+ padding: 16px;
+ font-size: 15px;
+ margin-bottom: 16px;
+}
+
+.start-btn .spin {
+ animation: spin 1s linear infinite;
+}
+
+/* Pro Tip */
+.pro-tip {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ padding: 14px;
+ background: rgba(99, 102, 241, 0.08);
+ border-radius: var(--radius-md);
+ font-size: 12px;
+ color: var(--color-text-secondary);
+ line-height: 1.5;
+}
+
+.pro-tip svg {
+ flex-shrink: 0;
+ color: var(--color-accent-primary);
+ margin-top: 2px;
+}
+
+/* Error Message */
+.error-message {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 14px;
+ background: rgba(239, 68, 68, 0.1);
+ border: 1px solid rgba(239, 68, 68, 0.2);
+ border-radius: var(--radius-md);
+ color: var(--color-danger);
+ font-size: 13px;
+ margin-top: 16px;
+}
+
+/* Model Info */
+.model-info {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 16px 24px;
+ background: var(--color-bg-tertiary);
+ border-top: 1px solid var(--color-border);
+}
+
+.model-badge {
+ width: 40px;
+ height: 40px;
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--color-accent-primary);
+}
+
+.model-details {
+ flex: 1;
+}
+
+.model-label {
+ display: block;
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.model-name {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+}
+
+.model-latency {
+ text-align: right;
+}
+
+.latency-label {
+ display: block;
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+}
+
+.latency-value {
+ font-size: 16px;
+ font-weight: 700;
+ color: var(--color-success);
+}
+
+/* Annotations Section */
+.annotations-section {
+ display: flex;
+ flex-direction: column;
+}
+
+.annotations-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ padding: 24px;
+ border-bottom: 1px solid var(--color-border);
+}
+
+.annotations-title {
+ font-size: 18px;
+ font-weight: 700;
+ color: var(--color-text-primary);
+ margin: 0 0 4px 0;
+}
+
+.annotations-subtitle {
+ font-size: 13px;
+ color: var(--color-text-muted);
+ margin: 0;
+}
+
+.annotations-actions {
+ display: flex;
+ gap: 8px;
+}
+
+/* Annotations Table */
+.annotations-table {
+ flex: 1;
+}
+
+.table-header {
+ display: grid;
+ grid-template-columns: 2fr 1fr 1fr 1fr 80px;
+ gap: 16px;
+ padding: 12px 24px;
+ background: var(--color-bg-tertiary);
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.table-body {
+ padding: 0 24px;
+}
+
+.table-row {
+ display: grid;
+ grid-template-columns: 2fr 1fr 1fr 1fr 80px;
+ gap: 16px;
+ padding: 16px 0;
+ align-items: center;
+ border-bottom: 1px solid var(--color-border);
+}
+
+.table-row:last-child {
+ border-bottom: none;
+}
+
+.col-filename {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.file-icon {
+ width: 36px;
+ height: 36px;
+ background: rgba(245, 158, 11, 0.1);
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--color-warning);
+}
+
+.file-icon.error {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--color-danger);
+}
+
+.filename {
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--color-text-primary);
+}
+
+.col-date {
+ font-size: 13px;
+ color: var(--color-text-muted);
+}
+
+.col-objects {
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--color-text-primary);
+}
+
+.col-status {
+ font-size: 13px;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.status-completed {
+ color: var(--color-success);
+}
+
+.status-processing {
+ color: var(--color-accent-primary);
+}
+
+.status-failed {
+ color: var(--color-danger);
+}
+
+.col-status .dot {
+ width: 6px;
+ height: 6px;
+ background: currentColor;
+ border-radius: 50%;
+}
+
+.col-status .dot.pulse {
+ animation: pulse 1.5s infinite;
+}
+
+.col-action {
+ display: flex;
+ justify-content: flex-end;
+}
+
+.action-btn {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--color-bg-tertiary);
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ color: var(--color-text-muted);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.action-btn:hover {
+ color: var(--color-text-primary);
+ border-color: var(--color-accent-primary);
+}
+
+/* Pagination */
+.annotations-pagination {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 24px;
+ border-top: 1px solid var(--color-border);
+}
+
+.page-info {
+ font-size: 13px;
+ color: var(--color-text-muted);
+}
+
+.page-controls {
+ display: flex;
+ gap: 8px;
+}
+
+.page-arrow {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ color: var(--color-text-muted);
+ cursor: pointer;
+}
+
+.page-arrow:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Footer Stats */
+.annotation-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 32px;
+ margin-top: 24px;
+ padding: 16px 24px;
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+}
+
+.footer-stat {
+ text-align: right;
+}
+
+.footer-stat .stat-label {
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ display: block;
+ margin-bottom: 4px;
+}
+
+.stat-value-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.footer-stat .stat-number {
+ font-size: 20px;
+ font-weight: 700;
+ color: var(--color-text-primary);
+}
+
+.footer-stat .stat-number.success {
+ color: var(--color-success);
+}
+
+.mini-progress {
+ width: 80px;
+ height: 6px;
+ background: var(--color-bg-tertiary);
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.mini-fill {
+ height: 100%;
+ background: var(--color-accent-primary);
+ border-radius: 3px;
+}
+
+/* Responsive */
+@media (max-width: 1000px) {
+ .annotation-layout {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 768px) {
+
+ .table-header,
+ .table-row {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .col-date,
+ .col-objects {
+ display: none;
+ }
+
+ .annotations-header {
+ flex-direction: column;
+ gap: 16px;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/pages/AutoAnnotationPage.jsx b/frontend/src/pages/AutoAnnotationPage.jsx
new file mode 100644
index 0000000..81665c4
--- /dev/null
+++ b/frontend/src/pages/AutoAnnotationPage.jsx
@@ -0,0 +1,297 @@
+import React, { useState } from "react";
+import {
+ Upload, FileText, CheckCircle, AlertCircle, Loader,
+ Download, Trash2, Eye, RefreshCw, Info, Zap, Cpu
+} from "lucide-react";
+import "./AutoAnnotationPage.css";
+
+const API_URL = "http://localhost:5000";
+
+const AutoAnnotationPage = () => {
+ const [files, setFiles] = useState([]);
+ const [uploading, setUploading] = useState(false);
+ const [processing, setProcessing] = useState(false);
+ const [results, setResults] = useState(null);
+ const [error, setError] = useState(null);
+ const [confidence, setConfidence] = useState(0.45);
+
+
+
+ const handleDragOver = (e) => {
+ e.preventDefault();
+ };
+
+ const handleDrop = (e) => {
+ e.preventDefault();
+ if (e.dataTransfer.files) {
+ setFiles(Array.from(e.dataTransfer.files));
+ }
+ };
+
+ const handleFileChange = (e) => {
+ if (e.target.files) {
+ setFiles(Array.from(e.target.files));
+ }
+ };
+
+ const handleUploadAndProcess = async () => {
+ if (files.length === 0) return;
+
+ setUploading(true);
+ setError(null);
+ setResults(null);
+
+ const formData = new FormData();
+ files.forEach((file) => {
+ formData.append("images", file);
+ });
+
+ try {
+ const uploadRes = await fetch(`${API_URL}/api/annotation/upload`, {
+ method: "POST",
+ body: formData,
+ });
+
+ if (!uploadRes.ok) throw new Error("Upload failed");
+
+ setUploading(false);
+ setProcessing(true);
+
+ const processRes = await fetch(`${API_URL}/api/annotation/process`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ confidence }),
+ });
+
+ if (!processRes.ok) throw new Error("Processing failed");
+
+ const data = await processRes.json();
+ setResults(data.results);
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setUploading(false);
+ setProcessing(false);
+ }
+ };
+
+ const getConfidenceLabel = () => {
+ if (confidence < 0.3) return "PRECISE";
+ if (confidence < 0.6) return "BALANCED";
+ return "LOOSE";
+ };
+
+ return (
+
+
+ {/* Left Panel - Upload Area */}
+
+
+
Auto-Annotation Tool
+
+ Upload images to automatically generate YOLO labels using our high-precision server model.
+
+
+ {/* Drop Zone */}
+
document.getElementById("fileInput").click()}
+ >
+
+
+
+
+
Drag & Drop images here
+
or click to browse from files
+
+ JPG
+ PNG
+ WEBP
+
+ {files.length > 0 && (
+
+ {files.length} images selected
+
+ )}
+
+
+ {/* Confidence Slider */}
+
+
+ Confidence Threshold
+ {confidence.toFixed(2)}
+
+
+
setConfidence(parseFloat(e.target.value))}
+ className="confidence-slider"
+ />
+
+ PRECISE
+ BALANCED
+ LOOSE
+
+
+
+
+ {/* Action Button */}
+
+
+ {/* Pro Tip */}
+
+
+ Pro tip: Higher confidence reduces false positives but might miss smaller baggage items in low lighting.
+
+
+ {error && (
+
+ )}
+
+
+ {/* Model Info Footer */}
+
+
+
+
+
+ CURRENT MODEL
+ YOLOv11n-Baggage v1.2.3
+
+
+ LATENCY
+ ~14ms
+
+
+
+
+ {/* Right Panel - Recent Annotations */}
+
+
+
+
Recent Annotations
+
Manage and export your processed imagery
+
+
+
+
+
+
+
+ {/* Annotations Table */}
+
+
+ IMAGE
+ FILENAME
+ LABEL FILE
+ ACTION
+
+
+ {results ? (
+ results.map((item, index) => (
+
+
+
+

+
+
+
+ {item.image}
+
+
+ {item.label}
+
+
+
+ ))
+ ) : (
+
+
No processed results yet. Upload images to start.
+
+ )}
+
+
+
+ {/* Pagination */}
+
+
Page 1 of 1
+
+
+
+
+
+
+
+
+ {/* Footer Stats */}
+
+
+
+ SYSTEM UPTIME
+ 99.8%
+
+
+
+ );
+};
+
+export default AutoAnnotationPage;
diff --git a/frontend/src/pages/CamerasPage.css b/frontend/src/pages/CamerasPage.css
new file mode 100644
index 0000000..97b0782
--- /dev/null
+++ b/frontend/src/pages/CamerasPage.css
@@ -0,0 +1,687 @@
+/* ===================================
+ CAMERAS PAGE STYLES
+ =================================== */
+
+.cameras-page {
+ max-width: 1600px;
+ margin: 0 auto;
+}
+
+/* Page Header */
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 24px;
+}
+
+.header-left .page-title {
+ margin-bottom: 4px;
+}
+
+.header-actions {
+ display: flex;
+ gap: 12px;
+}
+
+/* Layout Grid */
+.cameras-layout {
+ display: grid;
+ grid-template-columns: 280px 1fr;
+ gap: 24px;
+}
+
+/* Sidebar */
+.cameras-sidebar {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.sidebar-card {
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ padding: 20px;
+}
+
+.sidebar-card .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+ padding: 0;
+ border: none;
+}
+
+.sidebar-card .card-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+}
+
+.card-badge {
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--color-accent-primary);
+ background: rgba(99, 102, 241, 0.1);
+ padding: 4px 8px;
+ border-radius: 4px;
+}
+
+/* Project List */
+.project-list {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.project-item {
+ width: 100%;
+ padding: 12px 16px;
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: var(--radius-md);
+ text-align: left;
+ font-size: 14px;
+ color: var(--color-text-secondary);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.project-item:hover {
+ background: var(--color-bg-tertiary);
+ color: var(--color-text-primary);
+}
+
+.project-item.active {
+ background: rgba(99, 102, 241, 0.1);
+ border-color: var(--color-accent-primary);
+ color: var(--color-accent-primary);
+}
+
+.empty-text {
+ font-size: 13px;
+ color: var(--color-text-muted);
+ text-align: center;
+ padding: 20px;
+}
+
+/* Empty State */
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 80px 40px;
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ text-align: center;
+}
+
+.empty-state-icon {
+ font-size: 48px;
+ margin-bottom: 16px;
+}
+
+.empty-state-title {
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+ margin: 0 0 8px 0;
+}
+
+.empty-state-text {
+ font-size: 14px;
+ color: var(--color-text-muted);
+ margin: 0;
+}
+
+/* Airport Map */
+.airport-map {
+ position: relative;
+ height: 180px;
+ background: var(--color-bg-tertiary);
+ border: 1px solid var(--color-border);
+ border-radius: 10px;
+ overflow: hidden;
+}
+
+.map-content {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ padding: 16px;
+}
+
+.terminal-zone {
+ position: absolute;
+ width: 45%;
+ height: 50%;
+ border: 1px dashed var(--color-border);
+ border-radius: 8px;
+}
+
+.zone-a {
+ top: 10%;
+ left: 5%;
+}
+
+.zone-b {
+ top: 10%;
+ right: 5%;
+}
+
+.baggage-zone {
+ position: absolute;
+ bottom: 10%;
+ left: 25%;
+ width: 50%;
+ height: 25%;
+ border: 1px dashed var(--color-border);
+ border-radius: 8px;
+}
+
+.camera-marker {
+ position: absolute;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ border: 3px solid;
+ cursor: pointer;
+ transition: transform var(--transition-fast);
+}
+
+.camera-marker:hover {
+ transform: scale(1.2);
+}
+
+.camera-marker.active {
+ background: var(--color-success);
+ border-color: rgba(16, 185, 129, 0.3);
+ box-shadow: 0 0 12px rgba(16, 185, 129, 0.5);
+}
+
+.camera-marker.warning {
+ background: var(--color-warning);
+ border-color: rgba(245, 158, 11, 0.3);
+}
+
+.camera-marker.offline {
+ background: var(--color-danger);
+ border-color: rgba(239, 68, 68, 0.3);
+}
+
+.map-controls {
+ position: absolute;
+ right: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.map-btn {
+ width: 28px;
+ height: 28px;
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ font-size: 16px;
+ color: var(--color-text-secondary);
+}
+
+.map-btn:hover {
+ background: var(--color-bg-tertiary);
+}
+
+/* Zone Statistics */
+.zone-stats {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.zone-stat {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.zone-label {
+ font-size: 13px;
+ color: var(--color-text-muted);
+}
+
+.zone-value {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+}
+
+.zone-value .count {
+ font-size: 24px;
+ font-weight: 700;
+ color: var(--color-text-primary);
+}
+
+.zone-value .status {
+ font-size: 11px;
+ font-weight: 600;
+}
+
+.zone-value .status.active {
+ color: var(--color-success);
+}
+
+.zone-value .status.offline {
+ color: var(--color-danger);
+}
+
+/* Health Items */
+.health-items {
+ margin-top: 8px;
+}
+
+.health-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8px;
+}
+
+.health-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ color: var(--color-text-secondary);
+}
+
+.health-label .dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+}
+
+.health-label .dot.success {
+ background: var(--color-success);
+}
+
+.health-value {
+ font-size: 13px;
+ color: var(--color-text-muted);
+}
+
+/* Camera Grid */
+.cameras-grid-container {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.cameras-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 20px;
+}
+
+/* Camera Card */
+.camera-card {
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ transition: all var(--transition-normal);
+}
+
+.camera-card:hover {
+ border-color: rgba(99, 102, 241, 0.3);
+ box-shadow: var(--shadow-md);
+ transform: translateY(-2px);
+}
+
+.camera-preview {
+ position: relative;
+ aspect-ratio: 16/10;
+ background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
+ overflow: hidden;
+}
+
+.camera-preview img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.camera-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+}
+
+.camera-preview .detection-box {
+ position: absolute;
+ width: 80px;
+ height: 60px;
+ border: 2px solid var(--color-success);
+ border-radius: 4px;
+ background: rgba(16, 185, 129, 0.1);
+}
+
+.camera-preview .detection-label {
+ position: absolute;
+ top: -24px;
+ left: 0;
+ background: var(--color-success);
+ color: white;
+ font-size: 10px;
+ font-weight: 600;
+ padding: 2px 8px;
+ border-radius: 4px;
+ white-space: nowrap;
+}
+
+.camera-offline-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: #64748b;
+ gap: 12px;
+}
+
+.camera-offline-state span {
+ font-size: 12px;
+ font-weight: 600;
+ letter-spacing: 1px;
+}
+
+.camera-preview .status-badge {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 12px;
+ border-radius: 6px;
+ font-size: 11px;
+ font-weight: 700;
+}
+
+.camera-preview .status-badge.online {
+ background: rgba(16, 185, 129, 0.9);
+ color: white;
+}
+
+.camera-preview .status-badge.offline {
+ background: rgba(100, 116, 139, 0.9);
+ color: white;
+}
+
+.camera-preview .status-badge .dot {
+ width: 6px;
+ height: 6px;
+ background: white;
+ border-radius: 50%;
+ animation: pulse 2s infinite;
+}
+
+.expand-btn {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ width: 32px;
+ height: 32px;
+ background: rgba(255, 255, 255, 0.9);
+ border: none;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ opacity: 0;
+ transition: opacity var(--transition-fast);
+}
+
+.camera-card:hover .expand-btn {
+ opacity: 1;
+}
+
+/* Camera Info */
+.camera-info {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ padding: 16px;
+}
+
+.camera-details {
+ flex: 1;
+}
+
+.camera-name {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+ margin: 0 0 4px 0;
+}
+
+.camera-location {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ color: var(--color-text-muted);
+ margin: 0;
+}
+
+.camera-count {
+ text-align: right;
+}
+
+.camera-count .count-value {
+ font-size: 24px;
+ font-weight: 700;
+ color: var(--color-accent-primary);
+ line-height: 1;
+}
+
+.camera-count .count-label {
+ display: block;
+ font-size: 10px;
+ color: var(--color-text-muted);
+ margin-top: 2px;
+}
+
+/* Camera Footer */
+.camera-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ background: var(--color-bg-tertiary);
+ border-top: 1px solid var(--color-border);
+}
+
+.camera-tags {
+ display: flex;
+ gap: 8px;
+}
+
+.tag {
+ font-size: 11px;
+ font-weight: 600;
+ padding: 4px 10px;
+ border-radius: 4px;
+}
+
+.tag.normal {
+ background: rgba(16, 185, 129, 0.1);
+ color: var(--color-success);
+}
+
+.tag.detection {
+ background: rgba(99, 102, 241, 0.1);
+ color: var(--color-accent-primary);
+}
+
+.tag.high-volume {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--color-danger);
+}
+
+.tag.maintenance {
+ background: rgba(245, 158, 11, 0.1);
+ color: var(--color-warning);
+}
+
+.troubleshoot-btn {
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--color-accent-primary);
+ background: none;
+ border: none;
+ cursor: pointer;
+}
+
+.troubleshoot-btn:hover {
+ text-decoration: underline;
+}
+
+.settings-btn {
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ color: var(--color-text-muted);
+ cursor: pointer;
+}
+
+.settings-btn:hover {
+ color: var(--color-text-primary);
+ border-color: var(--color-accent-primary);
+}
+
+/* Pagination */
+.cameras-pagination {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 20px;
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+}
+
+.pagination-info {
+ font-size: 13px;
+ color: var(--color-text-muted);
+}
+
+.pagination-info strong {
+ color: var(--color-text-primary);
+}
+
+.pagination-controls {
+ display: flex;
+ gap: 8px;
+}
+
+.page-btn {
+ min-width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: 8px;
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--color-text-secondary);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.page-btn:hover:not(:disabled) {
+ border-color: var(--color-accent-primary);
+ color: var(--color-accent-primary);
+}
+
+.page-btn.active {
+ background: var(--color-accent-primary);
+ border-color: var(--color-accent-primary);
+ color: white;
+}
+
+.page-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.page-btn.grid-toggle {
+ background: var(--color-accent-primary);
+ border-color: var(--color-accent-primary);
+ color: white;
+}
+
+.grid-icon {
+ font-size: 16px;
+}
+
+/* Responsive */
+@media (max-width: 1200px) {
+ .cameras-layout {
+ grid-template-columns: 1fr;
+ }
+
+ .cameras-sidebar {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 16px;
+ }
+}
+
+@media (max-width: 900px) {
+ .cameras-sidebar {
+ grid-template-columns: 1fr;
+ }
+
+ .cameras-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 600px) {
+ .page-header {
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ .header-actions {
+ width: 100%;
+ }
+
+ .header-actions .btn {
+ flex: 1;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/pages/CamerasPage.jsx b/frontend/src/pages/CamerasPage.jsx
index 906d737..81d487d 100644
--- a/frontend/src/pages/CamerasPage.jsx
+++ b/frontend/src/pages/CamerasPage.jsx
@@ -1,165 +1,142 @@
-import { useState, useEffect } from "react";
+import React, { useState, useEffect } from "react";
import { API } from "../api/backend";
import {
- TextField,
- Button,
- MenuItem,
- Dialog,
- DialogActions,
- DialogContent,
- DialogTitle,
-} from "@mui/material";
+ Camera, MapPin, Filter, Plus, Settings, Wifi, WifiOff
+} from "lucide-react";
+import "./CamerasPage.css";
export default function CamerasPage() {
const [projects, setProjects] = useState([]);
- const [projectId, setProjectId] = useState("");
const [cameras, setCameras] = useState([]);
-
- const [cameraName, setCameraName] = useState("");
- const [rtspUrl, setRtspUrl] = useState("");
-
- const [editCamera, setEditCamera] = useState(null);
- const [editName, setEditName] = useState("");
- const [editUrl, setEditUrl] = useState("");
+ const [projectId, setProjectId] = useState("");
useEffect(() => {
- API.get("/projects").then((res) => setProjects(res.data));
+ API.get("/projects").then((res) => setProjects(res.data)).catch(() => { });
}, []);
const fetchCameras = async (projId) => {
- const res = await API.get(`/cameras/project/${projId}`);
- setCameras(res.data);
- };
-
- const createCamera = async () => {
- await API.post("/cameras", { projectId, cameraName, rtspUrl });
- setCameraName("");
- setRtspUrl("");
- fetchCameras(projectId);
- };
-
- const openEditDialog = (camera) => {
- setEditCamera(camera);
- setEditName(camera.cameraName);
- setEditUrl(camera.rtspUrl);
- };
-
- const updateCamera = async () => {
- await API.put(`/cameras/${editCamera.id}`, {
- cameraName: editName,
- rtspUrl: editUrl,
- });
- setEditCamera(null);
- fetchCameras(projectId);
- };
-
- const deleteCamera = async (id) => {
- await API.delete(`/cameras/${id}`);
- fetchCameras(projectId);
+ try {
+ const res = await API.get(`/cameras/project/${projId}`);
+ setCameras(res.data);
+ } catch (err) {
+ console.error("Failed to fetch cameras", err);
+ }
};
return (
-
-
Cameras
-
-
{
- setProjectId(e.target.value);
- fetchCameras(e.target.value);
- }}
- style={{ width: 300, marginBottom: 20 }}
- >
- {projects.map((project) => (
-
- ))}
-
-
-
-
-
setCameraName(e.target.value)}
- style={{ marginRight: 10 }}
- />
- setRtspUrl(e.target.value)}
- style={{ marginRight: 10 }}
- />
-
-
-
- Camera List
-
- {cameras.map((c) => (
-
-
{c.cameraName} — {c.rtspUrl}
-
-
-
-
-
-
+
+ {/* Page Header */}
+
+
+
+ {/* Left Sidebar */}
+
+
+ {/* Camera Grid */}
+
+ {cameras.length > 0 ? (
+
+ {cameras.map((camera) => (
+
+
+
+
+ CAMERA FEED
+
+
+
+ AVAILABLE
+
+
+
+
+
+
{camera.cameraName}
+
+
+ {camera.rtspUrl}
+
+
+
+
+
+
+ ))}
+
+ ) : (
+
+
📹
+
No Cameras
+
+ {projectId ? "No cameras found for this project." : "Select a project to view cameras."}
+
+
+ )}
+
+
);
}
diff --git a/frontend/src/pages/DashboardHostPage.css b/frontend/src/pages/DashboardHostPage.css
new file mode 100644
index 0000000..20399b0
--- /dev/null
+++ b/frontend/src/pages/DashboardHostPage.css
@@ -0,0 +1,172 @@
+/* ===================================
+ DASHBOARD HOST PAGE STYLES
+ =================================== */
+
+.dashboard-page {
+ max-width: 1400px;
+ margin: 0 auto;
+}
+
+/* Header */
+.dashboard-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 24px;
+}
+
+.header-content .page-title {
+ margin-bottom: 4px;
+}
+
+.header-stats {
+ display: flex;
+ gap: 24px;
+}
+
+.header-stat {
+ text-align: center;
+ padding: 12px 20px;
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+}
+
+.header-stat .stat-number {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--color-accent-primary);
+ line-height: 1;
+}
+
+.header-stat .stat-text {
+ font-size: 12px;
+ color: var(--color-text-muted);
+ display: block;
+ margin-top: 4px;
+}
+
+/* Control Panel */
+.control-panel {
+ padding: 20px 24px;
+ margin-bottom: 24px;
+ border-radius: var(--radius-lg);
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+}
+
+.control-panel-inner {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ margin-bottom: 16px;
+}
+
+.input-group {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ background: var(--color-bg-tertiary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ padding: 0 16px;
+ transition: all var(--transition-normal);
+}
+
+.input-group:focus-within {
+ border-color: var(--color-accent-primary);
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
+}
+
+.input-icon {
+ font-size: 18px;
+ color: var(--color-text-muted);
+}
+
+.input-group .input {
+ flex: 1;
+ background: transparent;
+ border: none;
+ padding: 14px 0;
+}
+
+.input-group .input:focus {
+ box-shadow: none;
+}
+
+.demo-btn {
+ white-space: nowrap;
+}
+
+/* Quick Actions */
+.quick-actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding-top: 16px;
+ border-top: 1px solid var(--color-border);
+}
+
+.quick-label {
+ font-size: 13px;
+ color: var(--color-text-muted);
+}
+
+/* Stream Grid */
+.stream-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
+ gap: 24px;
+}
+
+.stream-link {
+ text-decoration: none;
+ color: inherit;
+ display: block;
+}
+
+.stream-link:hover .card {
+ transform: translateY(-4px);
+ box-shadow: var(--shadow-lg);
+}
+
+/* Empty State */
+.empty-state {
+ padding: 80px 32px;
+ background: var(--color-bg-card);
+}
+
+.empty-state-icon {
+ font-size: 56px;
+ margin-bottom: 20px;
+}
+
+.empty-state-title {
+ font-size: 20px;
+ margin-bottom: 8px;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .dashboard-header {
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ .control-panel-inner {
+ flex-direction: column;
+ }
+
+ .input-group {
+ width: 100%;
+ }
+
+ .stream-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .quick-actions {
+ flex-wrap: wrap;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/pages/DashboardHostPage.jsx b/frontend/src/pages/DashboardHostPage.jsx
new file mode 100644
index 0000000..ba941af
--- /dev/null
+++ b/frontend/src/pages/DashboardHostPage.jsx
@@ -0,0 +1,157 @@
+import React, { useState } from "react";
+import { Link } from "react-router-dom";
+import StreamViewer from "../components/StreamViewer";
+import "./DashboardHostPage.css";
+
+const DashboardHostPage = () => {
+ const [sourceInput, setSourceInput] = useState("");
+ const [streams, setStreams] = useState([]);
+ const [loading, setLoading] = useState(false);
+
+ const startStream = async (sourceOverride = null) => {
+ const source = sourceOverride || sourceInput.trim();
+
+ if (!source) {
+ return alert("Please enter a RTSP URL, File Path, or '0' for webcam");
+ }
+
+ setLoading(true);
+ try {
+ const res = await fetch("http://localhost:5000/streams/start", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ source }),
+ });
+ const data = await res.json();
+
+ if (data.success) {
+ setStreams([...streams, { id: data.streamId, source: source, startedAt: new Date() }]);
+ if (!sourceOverride) setSourceInput("");
+ } else {
+ alert("Failed to start stream: " + (data.error || "Unknown error"));
+ }
+ } catch (err) {
+ alert("Error connecting to backend. Make sure the server is running on port 5000.");
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const stopStream = async (streamId) => {
+ try {
+ await fetch("http://localhost:5000/streams/stop", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ streamId }),
+ });
+ setStreams(streams.filter(s => s.id !== streamId));
+ } catch (err) {
+ console.error("Failed to stop stream", err);
+ }
+ };
+
+ const handleKeyPress = (e) => {
+ if (e.key === 'Enter' && !loading) {
+ startStream();
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ {/* Control Panel */}
+
+
+
+ 🔗
+ setSourceInput(e.target.value)}
+ onKeyPress={handleKeyPress}
+ disabled={loading}
+ />
+
+
+
+
+
+ Quick start:
+
+
+
+
+
+ {/* Stream Grid */}
+ {streams.length > 0 ? (
+
+ {streams.map((stream) => (
+
+
+
+ ))}
+
+ ) : (
+
+
🎥
+
No Active Streams
+
+ Add a video feed above to start real-time baggage detection analysis.
+
+
+ )}
+
+ );
+};
+
+export default DashboardHostPage;
diff --git a/frontend/src/pages/SettingsPage.css b/frontend/src/pages/SettingsPage.css
new file mode 100644
index 0000000..a05417f
--- /dev/null
+++ b/frontend/src/pages/SettingsPage.css
@@ -0,0 +1,547 @@
+/* ===================================
+ SETTINGS PAGE STYLES
+ =================================== */
+
+.settings-page {
+ max-width: 1400px;
+ margin: 0 auto;
+}
+
+/* Header */
+.settings-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 24px;
+}
+
+.settings-header .page-title {
+ margin-bottom: 4px;
+}
+
+.header-actions {
+ display: flex;
+ gap: 12px;
+}
+
+/* Layout */
+.settings-layout {
+ display: grid;
+ grid-template-columns: 240px 1fr;
+ gap: 24px;
+}
+
+/* Sidebar */
+.settings-sidebar {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.sidebar-nav {
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ padding: 8px;
+}
+
+.nav-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ padding: 12px 16px;
+ background: transparent;
+ border: none;
+ border-radius: var(--radius-md);
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--color-text-secondary);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ text-align: left;
+}
+
+.nav-item:hover {
+ background: var(--color-bg-tertiary);
+ color: var(--color-text-primary);
+}
+
+.nav-item.active {
+ background: rgba(99, 102, 241, 0.1);
+ color: var(--color-accent-primary);
+}
+
+/* System Status */
+.system-status {
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ padding: 16px;
+}
+
+.status-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--color-accent-primary);
+ margin-bottom: 16px;
+}
+
+.status-dot {
+ width: 8px;
+ height: 8px;
+ background: var(--color-success);
+ border-radius: 50%;
+ animation: pulse 2s infinite;
+}
+
+.status-item {
+ display: flex;
+ justify-content: space-between;
+ font-size: 12px;
+ padding: 8px 0;
+}
+
+.status-label {
+ color: var(--color-text-muted);
+}
+
+.status-value {
+ font-weight: 600;
+ color: var(--color-text-primary);
+}
+
+.status-value.success {
+ color: var(--color-success);
+}
+
+/* Settings Content */
+.settings-content {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.settings-section {
+ padding: 24px;
+}
+
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 24px;
+}
+
+.section-header h2 {
+ font-size: 18px;
+ font-weight: 700;
+ color: var(--color-text-primary);
+ margin: 0 0 4px 0;
+}
+
+.section-header p {
+ font-size: 13px;
+ color: var(--color-text-muted);
+ margin: 0;
+}
+
+/* Form Elements */
+.form-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 24px;
+ margin-bottom: 24px;
+}
+
+.form-group {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.form-group label {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--color-text-primary);
+}
+
+.value-badge {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--color-accent-primary);
+}
+
+.form-select,
+.form-input {
+ padding: 12px 14px;
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ font-size: 14px;
+ color: var(--color-text-primary);
+ font-family: inherit;
+ transition: all var(--transition-fast);
+}
+
+.form-select:focus,
+.form-input:focus {
+ outline: none;
+ border-color: var(--color-accent-primary);
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
+}
+
+.form-range {
+ width: 100%;
+ height: 6px;
+ -webkit-appearance: none;
+ appearance: none;
+ background: var(--color-bg-tertiary);
+ border-radius: 3px;
+ outline: none;
+}
+
+.form-range::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 18px;
+ height: 18px;
+ background: var(--color-accent-primary);
+ border-radius: 50%;
+ cursor: pointer;
+ box-shadow: 0 2px 6px rgba(99, 102, 241, 0.4);
+}
+
+.form-hint {
+ font-size: 11px;
+ color: var(--color-text-muted);
+ margin: 4px 0 0 0;
+ line-height: 1.4;
+}
+
+.input-with-suffix {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.input-with-suffix .form-input {
+ flex: 1;
+}
+
+.input-suffix {
+ font-size: 14px;
+ color: var(--color-text-muted);
+}
+
+/* Toggle Items */
+.toggle-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 16px;
+}
+
+.toggle-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px;
+ background: var(--color-bg-tertiary);
+ border-radius: var(--radius-md);
+}
+
+.toggle-info {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.toggle-label {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--color-text-primary);
+}
+
+.toggle-desc {
+ font-size: 12px;
+ color: var(--color-text-muted);
+}
+
+.toggle-switch {
+ width: 48px;
+ height: 26px;
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: 13px;
+ position: relative;
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.toggle-switch.active {
+ background: var(--color-accent-primary);
+ border-color: var(--color-accent-primary);
+}
+
+.toggle-knob {
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 20px;
+ height: 20px;
+ background: white;
+ border-radius: 50%;
+ transition: all var(--transition-fast);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.toggle-switch.active .toggle-knob {
+ left: calc(100% - 22px);
+}
+
+/* Subsection Title */
+.subsection-title {
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.8px;
+ margin: 24px 0 16px 0;
+}
+
+/* Notification Hooks */
+.notification-hooks {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.hook-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 16px;
+ background: var(--color-bg-tertiary);
+ border: 1px solid var(--color-border);
+ border-radius: 20px;
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--color-text-secondary);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.hook-item:hover {
+ border-color: var(--color-accent-primary);
+}
+
+.hook-item.active {
+ background: rgba(99, 102, 241, 0.1);
+ border-color: var(--color-accent-primary);
+ color: var(--color-accent-primary);
+}
+
+.hook-item .check-icon {
+ color: var(--color-success);
+}
+
+/* Users Table */
+.users-table {
+ margin-top: 16px;
+}
+
+.users-table .table-header {
+ display: grid;
+ grid-template-columns: 2fr 1fr 1fr 1fr 60px;
+ gap: 16px;
+ padding: 12px 16px;
+ background: var(--color-bg-tertiary);
+ border-radius: var(--radius-md);
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.users-table .table-row {
+ display: grid;
+ grid-template-columns: 2fr 1fr 1fr 1fr 60px;
+ gap: 16px;
+ padding: 16px;
+ align-items: center;
+ border-bottom: 1px solid var(--color-border);
+}
+
+.users-table .table-row:last-child {
+ border-bottom: none;
+}
+
+.user-cell {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.user-avatar {
+ width: 36px;
+ height: 36px;
+ background: var(--color-accent-gradient);
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ font-weight: 700;
+ color: white;
+}
+
+.user-info {
+ display: flex;
+ flex-direction: column;
+}
+
+.user-name {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+}
+
+.user-email {
+ font-size: 12px;
+ color: var(--color-text-muted);
+}
+
+.role-badge {
+ display: inline-block;
+ padding: 4px 10px;
+ border-radius: 4px;
+ font-size: 11px;
+ font-weight: 600;
+}
+
+.role-badge.super-admin {
+ background: rgba(99, 102, 241, 0.15);
+ color: var(--color-accent-primary);
+}
+
+.role-badge.operator {
+ background: rgba(16, 185, 129, 0.15);
+ color: var(--color-success);
+}
+
+.role-badge.viewer {
+ background: rgba(100, 116, 139, 0.15);
+ color: var(--color-text-secondary);
+}
+
+.last-active {
+ font-size: 13px;
+ color: var(--color-text-muted);
+}
+
+.users-table .status-badge {
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.users-table .status-badge.active {
+ color: var(--color-success);
+}
+
+.users-table .status-badge.inactive {
+ color: var(--color-text-muted);
+}
+
+.users-table .action-btn {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ color: var(--color-text-muted);
+ cursor: pointer;
+}
+
+.users-table .action-btn:hover {
+ border-color: var(--color-accent-primary);
+ color: var(--color-text-primary);
+}
+
+.view-all-link {
+ text-align: center;
+ padding-top: 16px;
+}
+
+.view-all-link a {
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--color-accent-primary);
+ text-decoration: none;
+}
+
+.view-all-link a:hover {
+ text-decoration: underline;
+}
+
+/* Responsive */
+@media (max-width: 1000px) {
+ .settings-layout {
+ grid-template-columns: 1fr;
+ }
+
+ .settings-sidebar {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 16px;
+ }
+
+ .sidebar-nav {
+ order: 1;
+ }
+
+ .system-status {
+ order: 2;
+ }
+}
+
+@media (max-width: 768px) {
+ .settings-header {
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ .header-actions {
+ width: 100%;
+ }
+
+ .form-grid,
+ .toggle-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .settings-sidebar {
+ grid-template-columns: 1fr;
+ }
+
+ .users-table .table-header,
+ .users-table .table-row {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .users-table .table-header span:nth-child(3),
+ .users-table .table-header span:nth-child(4),
+ .users-table .table-row .last-active,
+ .users-table .table-row .status-badge:not(.role-badge) {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/pages/SettingsPage.jsx b/frontend/src/pages/SettingsPage.jsx
new file mode 100644
index 0000000..007e31f
--- /dev/null
+++ b/frontend/src/pages/SettingsPage.jsx
@@ -0,0 +1,233 @@
+import React, { useState } from "react";
+import {
+ Brain, Settings, Users, Shield, Check,
+ Mail, Webhook, MessageSquare
+} from "lucide-react";
+import "./SettingsPage.css";
+
+const SettingsPage = () => {
+ const [activeTab, setActiveTab] = useState("ai-config");
+ const [modelArchitecture, setModelArchitecture] = useState("yolo-v11-small");
+ const [inferenceEngine, setInferenceEngine] = useState("tensorrt");
+ const [confidenceThreshold, setConfidenceThreshold] = useState(0.45);
+ const [nmsThreshold, setNmsThreshold] = useState(0.25);
+ const [edgeDetection, setEdgeDetection] = useState(true);
+ const [autoRetraining, setAutoRetraining] = useState(false);
+ const [historyRetention, setHistoryRetention] = useState(30);
+ const [exportFormat, setExportFormat] = useState("csv");
+
+ const tabs = [
+ { id: "ai-config", label: "AI Model Config", icon: Brain },
+ { id: "general", label: "General Settings", icon: Settings },
+ ];
+
+ return (
+
+ {/* Page Header */}
+
+
+
+ {/* Sidebar Navigation */}
+
+
+ {/* Main Content */}
+
+ {/* AI Model Configuration */}
+ {activeTab === "ai-config" && (
+
+
+
AI Model Configuration
+
Select and tune the baggage detection model parameters.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
setConfidenceThreshold(parseFloat(e.target.value))}
+ className="form-range"
+ />
+
Minimum confidence score required for an object to be detected as baggage.
+
+
+
+
+
setNmsThreshold(parseFloat(e.target.value))}
+ className="form-range"
+ />
+
Threshold for Non-Maximum Suppression to handle overlapping detection boxes.
+
+
+
+
+
+
+ Enable Edge Detection
+ Run model on edge gateway devices
+
+
+
+
+
+
+ Automatic Retraining
+ Use flagged false-positives for training
+
+
+
+
+
+ )}
+
+ {/* General Settings */}
+ {activeTab === "general" && (
+
+
+
General Settings
+
Configure global application behavior and notifications.
+
+
+ REPORTING & DATA
+
+
+
+
+ setHistoryRetention(e.target.value)}
+ className="form-input"
+ />
+ Days
+
+
+
+
+
+
+
+
+
+ SYSTEM NOTIFICATION HOOKS
+
+
+
+ Email Alerts
+
+
+
+ Webhook URL
+
+
+
+ SMS Notifications
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default SettingsPage;
diff --git a/frontend/src/pages/SingleFeedDashboard.css b/frontend/src/pages/SingleFeedDashboard.css
new file mode 100644
index 0000000..12a0a27
--- /dev/null
+++ b/frontend/src/pages/SingleFeedDashboard.css
@@ -0,0 +1,608 @@
+/* ===================================
+ SINGLE FEED DASHBOARD STYLES
+ =================================== */
+
+.single-feed-dashboard {
+ max-width: 1600px;
+ margin: 0 auto;
+}
+
+/* Dashboard Layout Grid */
+.dashboard-layout {
+ display: grid;
+ grid-template-columns: 1fr 340px;
+ gap: 24px;
+}
+
+.main-column {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.side-column {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+/* Feed Section */
+.feed-section {
+ overflow: hidden;
+}
+
+.feed-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--color-border);
+}
+
+.feed-title {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+}
+
+.feed-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.icon-btn-sm {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--color-bg-tertiary);
+ border: none;
+ border-radius: 6px;
+ color: var(--color-text-muted);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.icon-btn-sm:hover {
+ background: var(--color-bg-primary);
+ color: var(--color-text-primary);
+}
+
+/* Video Container */
+.video-container {
+ position: relative;
+ aspect-ratio: 16 / 9;
+ background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
+ overflow: hidden;
+}
+
+.video-feed {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+}
+
+.video-placeholder {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 20px;
+ color: #94a3b8;
+}
+
+/* Count Overlay */
+.count-overlay {
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ background: rgba(255, 255, 255, 0.95);
+ backdrop-filter: blur(12px);
+ border-radius: 12px;
+ padding: 16px 24px;
+ text-align: center;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
+ z-index: 10;
+}
+
+.count-label {
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 1px;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+}
+
+.count-value {
+ font-size: 48px;
+ font-weight: 800;
+ color: var(--color-text-primary);
+ line-height: 1;
+ margin: 4px 0;
+}
+
+.count-change {
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.count-change.up {
+ color: var(--color-success);
+}
+
+.count-change.down {
+ color: var(--color-danger);
+}
+
+/* Detection Overlay */
+.detection-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+}
+
+.detection-box {
+ position: absolute;
+ border: 2px solid var(--color-success);
+ border-radius: 4px;
+ background: rgba(16, 185, 129, 0.1);
+}
+
+.detection-box.warning {
+ border-color: var(--color-warning);
+ background: rgba(245, 158, 11, 0.1);
+}
+
+.detection-label {
+ position: absolute;
+ top: -24px;
+ left: 0;
+ background: var(--color-success);
+ color: white;
+ font-size: 10px;
+ font-weight: 600;
+ padding: 2px 8px;
+ border-radius: 4px;
+ white-space: nowrap;
+}
+
+.detection-box.warning .detection-label {
+ background: var(--color-warning);
+}
+
+/* Chart Section */
+.chart-section {
+ padding: 0;
+}
+
+.chart-container {
+ padding: 20px;
+}
+
+.chart-tabs {
+ display: flex;
+ gap: 4px;
+ background: var(--color-bg-tertiary);
+ padding: 4px;
+ border-radius: 8px;
+}
+
+.tab-btn {
+ padding: 8px 16px;
+ font-size: 13px;
+ font-weight: 500;
+ border-radius: 6px;
+ background: transparent;
+ border: none;
+ color: var(--color-text-muted);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.tab-btn.active {
+ background: white;
+ color: var(--color-text-primary);
+ box-shadow: var(--shadow-sm);
+}
+
+.tab-btn:hover:not(.active) {
+ color: var(--color-text-primary);
+}
+
+.chart-legend {
+ display: flex;
+ justify-content: center;
+ gap: 24px;
+ margin-top: 16px;
+ padding-top: 16px;
+ border-top: 1px solid var(--color-border);
+}
+
+.legend-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ color: var(--color-text-secondary);
+}
+
+.legend-dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+}
+
+/* Status Section */
+.status-section {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.status-section .metric-card {
+ padding: 20px;
+}
+
+.metric-card .metric-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8px;
+}
+
+.metric-card .metric-label {
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--color-text-muted);
+}
+
+.metric-card .metric-value {
+ font-size: 36px;
+ font-weight: 700;
+ color: var(--color-text-primary);
+ line-height: 1.1;
+}
+
+.metric-card .metric-subtext {
+ font-size: 12px;
+ color: var(--color-text-muted);
+ margin-top: 2px;
+}
+
+.metric-card .metric-trend {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 13px;
+ font-weight: 600;
+ margin-top: 12px;
+}
+
+.metric-card .metric-trend.up {
+ color: var(--color-success);
+}
+
+.metric-card .metric-trend.warning {
+ color: var(--color-warning);
+}
+
+/* Performance Section */
+.performance-section {
+ padding: 0;
+}
+
+.performance-content {
+ padding: 16px 20px;
+}
+
+.perf-item {
+ padding: 16px 0;
+ border-bottom: 1px solid var(--color-border);
+}
+
+.perf-item:first-child {
+ padding-top: 0;
+}
+
+.perf-item:last-child {
+ border-bottom: none;
+}
+
+.perf-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8px;
+}
+
+.perf-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ color: var(--color-text-secondary);
+}
+
+.perf-badge {
+ font-size: 10px;
+ font-weight: 700;
+ color: var(--color-success);
+ text-transform: uppercase;
+}
+
+.perf-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ margin-bottom: 8px;
+}
+
+.perf-value {
+ display: flex;
+ align-items: baseline;
+ gap: 4px;
+}
+
+.perf-value .number {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--color-text-primary);
+}
+
+.perf-value .unit {
+ font-size: 14px;
+ color: var(--color-text-muted);
+}
+
+.perf-meta {
+ display: flex;
+ justify-content: space-between;
+ font-size: 11px;
+ color: var(--color-text-muted);
+ margin-bottom: 8px;
+}
+
+/* System Info Footer */
+.system-info {
+ padding: 16px 20px;
+ background: var(--color-bg-tertiary);
+ border-top: 1px solid var(--color-border);
+}
+
+.info-row {
+ display: flex;
+ justify-content: space-between;
+ font-size: 12px;
+ padding: 6px 0;
+ color: var(--color-text-muted);
+}
+
+.info-row .link {
+ color: var(--color-accent-primary);
+ cursor: pointer;
+}
+
+/* Locations Section */
+.locations-section {
+ margin-top: 0;
+}
+
+.locations-grid {
+ display: grid;
+ grid-template-columns: 200px repeat(4, 1fr);
+ gap: 16px;
+}
+
+/* Airport Layout */
+.airport-layout {
+ grid-row: span 2;
+ padding: 16px;
+}
+
+.layout-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+ margin-bottom: 16px;
+}
+
+.layout-map {
+ position: relative;
+ height: 160px;
+ background: var(--color-bg-tertiary);
+ border: 1px solid var(--color-border);
+ border-radius: 8px;
+ padding: 12px;
+}
+
+.terminal {
+ position: relative;
+ font-size: 9px;
+ font-weight: 600;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+}
+
+.terminal-a {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+}
+
+.terminal-b {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+}
+
+.baggage-area {
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+ right: 10px;
+ text-align: center;
+}
+
+.camera-dot {
+ position: absolute;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ border: 3px solid;
+}
+
+.camera-dot.active {
+ background: var(--color-success);
+ border-color: rgba(16, 185, 129, 0.3);
+ box-shadow: 0 0 12px rgba(16, 185, 129, 0.5);
+}
+
+.camera-dot.offline {
+ background: var(--color-danger);
+ border-color: rgba(239, 68, 68, 0.3);
+}
+
+/* Camera Location Card */
+.camera-location-card {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 16px;
+}
+
+.camera-icon-wrapper {
+ width: 44px;
+ height: 44px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--color-bg-tertiary);
+ border-radius: 10px;
+}
+
+.camera-icon-wrapper .online {
+ color: var(--color-success);
+}
+
+.camera-icon-wrapper .offline {
+ color: var(--color-text-muted);
+}
+
+.camera-info {
+ flex: 1;
+}
+
+.camera-name {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+ margin-bottom: 2px;
+}
+
+.camera-loc {
+ font-size: 12px;
+ color: var(--color-text-muted);
+ margin-bottom: 6px;
+}
+
+.status-badge {
+ font-size: 10px;
+ font-weight: 600;
+ padding: 3px 8px;
+ border-radius: 4px;
+ text-transform: uppercase;
+}
+
+.status-badge.online {
+ background: rgba(16, 185, 129, 0.1);
+ color: var(--color-success);
+}
+
+.status-badge.offline {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--color-danger);
+}
+
+.camera-count {
+ text-align: right;
+}
+
+.camera-count .count-num {
+ font-size: 24px;
+ font-weight: 700;
+ color: var(--color-accent-primary);
+}
+
+.camera-count .count-label {
+ display: block;
+ font-size: 10px;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+}
+
+/* Responsive */
+@media (max-width: 1400px) {
+ .locations-grid {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ .airport-layout {
+ grid-row: auto;
+ grid-column: span 3;
+ }
+}
+
+@media (max-width: 1200px) {
+ .dashboard-layout {
+ grid-template-columns: 1fr;
+ }
+
+ .side-column {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 16px;
+ }
+
+ .status-section {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 12px;
+ }
+}
+
+@media (max-width: 768px) {
+ .locations-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .airport-layout {
+ grid-column: auto;
+ }
+
+ .side-column {
+ grid-template-columns: 1fr;
+ }
+
+ .status-section {
+ grid-template-columns: 1fr;
+ }
+
+ .count-overlay {
+ padding: 12px 16px;
+ }
+
+ .count-value {
+ font-size: 32px;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/pages/SingleFeedDashboard.jsx b/frontend/src/pages/SingleFeedDashboard.jsx
new file mode 100644
index 0000000..19384c1
--- /dev/null
+++ b/frontend/src/pages/SingleFeedDashboard.jsx
@@ -0,0 +1,274 @@
+import React, { useEffect, useState, useRef } from "react";
+import { useParams, Link } from "react-router-dom";
+import {
+ AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer
+} from "recharts";
+import {
+ Activity, ArrowLeft, TrendingUp, Settings, Wifi, AlertTriangle, Play, Cpu
+} from "lucide-react";
+import "./SingleFeedDashboard.css";
+
+const SingleFeedDashboard = () => {
+ const { streamId } = useParams();
+ const [imageSrc, setImageSrc] = useState(null);
+ const [currentCount, setCurrentCount] = useState(0);
+ const [status, setStatus] = useState("connecting");
+ const [fps, setFps] = useState(0);
+ const [historicalData, setHistoricalData] = useState([]);
+ const ws = useRef(null);
+ const frameTimestamps = useRef([]);
+
+ // Data generation for charts based on real detections
+ useEffect(() => {
+ const interval = setInterval(() => {
+ const now = new Date();
+ const timeStr = `${now.getHours()}:${now.getMinutes().toString().padStart(2, '0')}`;
+
+ setHistoricalData(prev => {
+ const newData = [...prev, {
+ time: timeStr,
+ count: currentCount
+ }];
+ if (newData.length > 20) return newData.slice(1);
+ return newData;
+ });
+ }, 10000);
+ return () => clearInterval(interval);
+ }, [currentCount]);
+
+ useEffect(() => {
+ ws.current = new WebSocket("ws://localhost:8081");
+
+ ws.current.onopen = () => setStatus("live");
+
+ ws.current.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data);
+ if (data.streamId === streamId) {
+ setImageSrc(`data:image/jpeg;base64,${data.image}`);
+ setCurrentCount(data.count || 0);
+
+ // FPS Calc
+ const now = Date.now();
+ frameTimestamps.current.push(now);
+ frameTimestamps.current = frameTimestamps.current.filter(t => now - t < 1000);
+ setFps(frameTimestamps.current.length);
+ }
+ } catch (err) {
+ console.error("WS Parse Error", err);
+ }
+ };
+
+ ws.current.onclose = () => setStatus("disconnected");
+
+ return () => {
+ if (ws.current) ws.current.close();
+ };
+ }, [streamId]);
+
+ const handleStopStream = async () => {
+ try {
+ await fetch("http://localhost:5000/streams/stop", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ streamId }),
+ });
+ setStatus("stopped");
+ if (ws.current) ws.current.close();
+ } catch (err) {
+ console.error("Failed to stop stream", err);
+ }
+ };
+
+ return (
+
+ {/* Main Grid */}
+
+ {/* Left Column - Video & Charts */}
+
+ {/* Live Feed Card */}
+
+
+
+
+
+
+
Live Feed - {streamId.slice(0, 8)}
+
+ {status.toUpperCase()}
+
+
+
+
+
+
+ {imageSrc ? (
+

+ ) : (
+
+
+
Establishing Secure Connection...
+
+ )}
+
+ {/* Baggage Count Overlay */}
+
+
CURRENT COUNT
+
{currentCount}
+
+
+
+
+ {/* Historical Trends Chart */}
+
+
+
Historical Trends - Today
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Right Column - Status & Performance */}
+
+ {/* Current Status Section */}
+
+ CURRENT STATUS
+
+ {/* Current Baggage Card */}
+
+
+ Current Baggage
+ Live
+
+
{currentCount}
+
Items detected
+
+
+ {/* Stream Status Card */}
+
+
+ Stream Status
+
+ {status === 'live' ? 'Connected' : 'Waiting'}
+
+
+
{streamId.slice(0, 8)}
+
Stream ID
+
+
+
+ {/* System Performance Section */}
+
+ SYSTEM PERFORMANCE
+
+ {/* Detection FPS */}
+
+
+
+
{fps > 20 ? 'GOOD' : fps > 10 ? 'FAIR' : 'LOW'}
+
+
+
+
+
+ {/* Network Status */}
+
+
+
+
+ Connection
+
+
{status === 'live' ? 'ACTIVE' : 'WAITING'}
+
+
+
+ {status === 'live' ? 'Connected' : 'Connecting'}
+
+
+
+
+
+ {/* System Info Footer */}
+
+
+ Model:
+ YOLOv11
+
+
+ WebSocket:
+ ws://localhost:8081
+
+
+
+
+
+
+ );
+};
+
+export default SingleFeedDashboard;
diff --git a/python-engine/auto_annotate.py b/python-engine/auto_annotate.py
new file mode 100644
index 0000000..2ae731b
--- /dev/null
+++ b/python-engine/auto_annotate.py
@@ -0,0 +1,89 @@
+import os
+import cv2
+import argparse
+from ultralytics import YOLO
+from pathlib import Path
+
+# CONFIG
+SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
+DEFAULT_MODEL_PATH = os.path.join(SCRIPT_DIR, "best.pt")
+
+def auto_annotate(input_dir, output_dir, model_path, conf_threshold):
+ # Load model
+ print(f"Loading model from {model_path}...")
+ model = YOLO(model_path)
+
+ # Create output directories
+ output_path = Path(output_dir)
+ labels_path = output_path / "labels"
+ visualized_path = output_path / "visualized"
+
+ labels_path.mkdir(parents=True, exist_ok=True)
+ visualized_path.mkdir(parents=True, exist_ok=True)
+
+ # Supported image extensions
+ image_extensions = [".jpg", ".jpeg", ".png", ".bmp", ".webp"]
+
+ input_path = Path(input_dir)
+ images = [f for f in input_path.iterdir() if f.suffix.lower() in image_extensions]
+
+ if not images:
+ print(f"No images found in {input_dir}")
+ return
+
+ print(f"Found {len(images)} images. Starting auto-annotation...")
+
+ for img_file in images:
+ print(f"Processing {img_file.name}...")
+
+ # Read image
+ frame = cv2.imread(str(img_file))
+ if frame is None:
+ print(f" Warning: Could not read {img_file.name}")
+ continue
+
+ h, w, _ = frame.shape
+
+ # Run inference
+ results = model(frame, conf=conf_threshold, verbose=False)[0]
+
+ # Generate YOLO format txt:
+ # Normalized to 0.0 - 1.0
+ label_file = labels_path / f"{img_file.stem}.txt"
+
+ with open(label_file, "w") as f:
+ for box in results.boxes:
+ # Get coordinates in xywh (normalized)
+ xywhn = box.xywhn[0].tolist()
+ cls_id = int(box.cls[0])
+
+ # Write to file
+ line = f"{cls_id} {' '.join(map(str, xywhn))}\n"
+ f.write(line)
+
+ # Draw on visualized copy
+ x1, y1, x2, y2 = map(int, box.xyxy[0])
+ conf = float(box.conf[0])
+ class_name = model.names.get(cls_id, f"Class {cls_id}")
+
+ color = (0, 255, 0) # Green
+ cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
+ label = f"{class_name} {conf:.2f}"
+ cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
+
+ # Save visualized image
+ cv2.imwrite(str(visualized_path / img_file.name), frame)
+
+ print(f"Done! Annotations saved to {labels_path}")
+ print(f"Visualizations saved to {visualized_path}")
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Auto-annotate images using a YOLO model")
+ parser.add_argument("--input", type=str, required=True, help="Directory containing images to annotate")
+ parser.add_argument("--output", type=str, default="auto_annotations", help="Directory to save labels and visualizations")
+ parser.add_argument("--model", type=str, default=DEFAULT_MODEL_PATH, help="Path to the YOLO model (.pt)")
+ parser.add_argument("--conf", type=float, default=0.25, help="Confidence threshold for detections")
+
+ args = parser.parse_args()
+
+ auto_annotate(args.input, args.output, args.model, args.conf)
diff --git a/python-engine/live_yolo_engine.py b/python-engine/live_yolo_engine.py
index 96dd49a..188daf2 100644
--- a/python-engine/live_yolo_engine.py
+++ b/python-engine/live_yolo_engine.py
@@ -1,60 +1,135 @@
import cv2
import base64
-import requests
+import json
+import argparse
+import os
import websocket
from ultralytics import YOLO
# CONFIG
-BACKEND = "http://localhost:3000"
WS_URL = "ws://localhost:8081"
+SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
+MODEL_PATH = os.path.join(SCRIPT_DIR, "best.pt")
-# Connect to WebSocket
-ws = websocket.WebSocket()
-ws.connect(WS_URL)
-print("Connected to WebSocket")
+# Load YOLO model globally or in main? Better in start_yolo_stream or main
+model = None
-# Load YOLO model
-model = YOLO("best.pt") # your trained model
+import time
+def start_yolo_stream(source, stream_id):
+ global model
+ if model is None:
+ print(f"Loading YOLO model from {MODEL_PATH}...")
+ model = YOLO(MODEL_PATH)
+
+ print(f"Starting persistent engine for {stream_id} with source: {source}")
+
+ # Expand source to int if it's a digit (for webcam index)
+ if source.isdigit():
+ source = int(source)
-def get_camera_url(camera_id):
- data = requests.get(f"{BACKEND}/cameras/{camera_id}").json()
- return data["rtspUrl"] # your backend uses rtspUrl key
+ while True: # Outer loop for overall process persistence
+ ws = None
+ try:
+ # 1. Connect to WebSocket
+ print(f"Connecting to WebSocket at {WS_URL}...")
+ ws = websocket.WebSocket()
+ ws.connect(WS_URL)
+ print("Connected to WebSocket")
+ # 2. Open Video Source
+ cap = cv2.VideoCapture(source)
+ if not cap.isOpened():
+ print(f"Failed to open source: {source}. Retrying in 5 seconds...")
+ time.sleep(5)
+ continue
-def start_yolo_stream(camera_id):
- cam_url = get_camera_url(camera_id)
- print("Opening Camera:", cam_url)
+ print(f"Source {source} opened successfully")
- cap = cv2.VideoCapture(cam_url)
+ frame_count = 0
+ while True: # Inner loop for frame processing
+ ret, frame = cap.read()
+ if not ret:
+ print("Cannot read frame. Source might be disconnected. Retrying...")
+ # Try to release and re-open the capture
+ cap.release()
+ time.sleep(2)
+ cap = cv2.VideoCapture(source)
+ if not cap.isOpened():
+ break # Break inner loop to restart everything if source is truly gone
+ continue
- while True:
- ret, frame = cap.read()
- if not ret:
- print("Cannot read frame")
- continue
+ frame_count += 1
+ # OPTIMIZATION: Process every 3rd frame (skip 2)
+ if frame_count % 3 != 0:
+ continue
- # Run YOLO detection
- results = model(frame)[0]
- for box in results.boxes:
- x1, y1, x2, y2 = box.xyxy[0]
- cv2.rectangle(
- frame,
- (int(x1), int(y1)),
- (int(x2), int(y2)),
- (0, 255, 0), 2
- )
+ # OPTIMIZATION: Resize to 640 width (maintain aspect ratio)
+ h, w = frame.shape[:2]
+ if w > 640:
+ scale = 640 / w
+ new_h = int(h * scale)
+ frame = cv2.resize(frame, (640, new_h))
- # Encode to JPEG
- success, buffer = cv2.imencode(".jpg", frame)
- if not success:
- continue
+ # Run YOLO detection
+ results = model(frame, verbose=False)[0]
+ for box in results.boxes:
+ x1, y1, x2, y2 = map(int, box.xyxy[0])
+ conf = float(box.conf[0])
+ cls_id = int(box.cls[0])
+ class_name = model.names.get(cls_id, f"Class {cls_id}")
+
+ # Color based on confidence
+ if conf > 0.7:
+ color = (0, 255, 0)
+ elif conf > 0.5:
+ color = (0, 255, 255)
+ else:
+ color = (0, 165, 255)
+
+ cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
+ label = f"{class_name} {conf:.0%}"
+ (label_w, label_h), baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
+ cv2.rectangle(frame, (x1, y1 - label_h - 10), (x1 + label_w + 10, y1), color, -1)
+ cv2.putText(frame, label, (x1 + 5, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
- jpg_base64 = base64.b64encode(buffer).decode("utf-8")
+ # Encode to JPEG with lower quality for speed
+ success, buffer = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
+ if not success:
+ continue
- # Send frame to Node WebSocket
- ws.send(jpg_base64)
+ jpg_base64 = base64.b64encode(buffer).decode("utf-8")
+ # Create JSON payload
+ payload = {
+ "streamId": stream_id,
+ "image": jpg_base64,
+ "count": len(results.boxes),
+ "timestamp": time.time()
+ }
-# Start streaming for camera ID 5
-start_yolo_stream(camera_id=5)
+ # Send frame to Node WebSocket
+ try:
+ ws.send(json.dumps(payload))
+ except Exception as e:
+ print(f"WebSocket send error: {e}. Attempting to reconnect...")
+ break # Break inner loop to reconnect WebSocket
+
+ cap.release()
+ except Exception as e:
+ print(f"Process Error: {e}. Restarting in 5 seconds...")
+ time.sleep(5)
+ finally:
+ if ws:
+ try:
+ ws.close()
+ except:
+ pass
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--source", type=str, required=True, help="RTSP URL or Video File Path")
+ parser.add_argument("--streamId", type=str, required=True, help="Unique ID for this stream session")
+ args = parser.parse_args()
+
+ start_yolo_stream(args.source, args.streamId)
diff --git a/video/test_video.mp4 b/video/test_video.mp4
new file mode 100644
index 0000000..6e37785
Binary files /dev/null and b/video/test_video.mp4 differ