diff --git a/README.md b/README.md new file mode 100644 index 0000000..29c3b6e --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# Baggage Platform + +This project is a comprehensive baggage tracking and analytics platform consisting of a Node.js backend, a React frontend, and a Python-based YOLO object detection engine. + +## Project Structure + +- **backend/**: Node.js Express server with Prisma ORM and WebSocket server. +- **frontend/**: React application for scanning and monitoring dashboard. +- **python-engine/**: Python script using YOLOv8 for real-time object detection and streaming. + +## Prerequisites + +- [Node.js](https://nodejs.org/) (v16 or higher) +- [Python](https://www.python.org/) (v3.8 or higher) +- [PostgreSQL](https://www.postgresql.org/) + +## Setup Instructions + +### 1. Database Setup (PostgreSQL) + +1. Install and start PostgreSQL. +2. Create a database (e.g., `baggage_db`). +3. Note your database connection details (username, password, host, port, database name). + +### 2. Backend Setup + +1. Navigate to the `backend` directory: + ```bash + cd backend + ``` +2. Install dependencies: + ```bash + npm install + ``` +3. Configure environment variables: + - Create a `.env` file in the `backend` directory (if it does not exist). + - Add the `DATABASE_URL` variable: + ```env + DATABASE_URL="postgresql://USER:PASSWORD@localhost:5432/baggage_db?schema=public" + ``` + **Note:** Replace `USER`, `PASSWORD`, and `baggage_db` with your actual PostgreSQL credentials. +4. Initialize the database schema: + ```bash + npx prisma generate + npx prisma db push + ``` +5. Start the backend server: + ```bash + npm start + ``` + - The API server runs on `http://localhost:5000`. + - The WebSocket server runs on `ws://localhost:8081`. + +### 3. Frontend Setup + +1. Open a new terminal and navigate to the `frontend` directory: + ```bash + cd frontend + ``` +2. Install dependencies: + ```bash + npm install + ``` +3. Start the development server: + ```bash + npm start + ``` + - The application will open at `http://localhost:3000`. + +### 4. Python Engine Setup + +1. Open a new terminal and navigate to the `python-engine` directory: + ```bash + cd python-engine + ``` +2. Install Python dependencies: + ```bash + pip install -r requirements.txt + ``` +3. Run the detection engine: + ```bash + python live_yolo_engine.py --source --streamId + ``` + - `--source`: Path to a video file or camera index (e.g., `0` for webcam). + - `--streamId`: A unique identifier for the stream (e.g., `cam1`). + + **Example:** + ```bash + python live_yolo_engine.py --source 0 --streamId cam1 + ``` + +## Running the Complete System + +1. Ensure the **PostgreSQL Database** is running. +2. Start the **Backend** (`npm start` in `backend/`). +3. Start the **Frontend** (`npm start` in `frontend/`). +4. Run the **Python Engine** with your desired video source. +5. Open `http://localhost:3000` to view the dashboard and real-time analytics. diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..122e439 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1 @@ +DATABASE_URL="postgresql://postgres:1234@localhost:5432/baggage_db?schema=public" 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/labels/1769096617682-frame_0027.txt b/backend/current_annotation_output/labels/1769096617682-frame_0027.txt new file mode 100644 index 0000000..b55c5a6 --- /dev/null +++ b/backend/current_annotation_output/labels/1769096617682-frame_0027.txt @@ -0,0 +1,4 @@ +0 0.6010957956314087 0.8095666170120239 0.1622653603553772 0.20247666537761688 +0 0.21071714162826538 0.6001806259155273 0.10528070479631424 0.1516542285680771 +0 0.37665608525276184 0.6881519556045532 0.1307695060968399 0.16416846215724945 +0 0.27681371569633484 0.3579447865486145 0.08090914040803909 0.10942911356687546 diff --git a/backend/current_annotation_output/labels/1769097003691-test gunny.txt b/backend/current_annotation_output/labels/1769097003691-test gunny.txt new file mode 100644 index 0000000..e69de29 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/current_annotation_output/visualized/1769096617682-frame_0027.jpg b/backend/current_annotation_output/visualized/1769096617682-frame_0027.jpg new file mode 100644 index 0000000..74006c0 Binary files /dev/null and b/backend/current_annotation_output/visualized/1769096617682-frame_0027.jpg differ diff --git a/backend/current_annotation_output/visualized/1769097003691-test gunny.jpeg b/backend/current_annotation_output/visualized/1769097003691-test gunny.jpeg new file mode 100644 index 0000000..c4449e3 Binary files /dev/null and b/backend/current_annotation_output/visualized/1769097003691-test gunny.jpeg 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/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 ? ( + Live Detection Feed + ) : ( +
+
+ Initializing YOLO model... +
+ )} + + {/* Overlay Stats */} + {imageSrc && ( +
+
+ {fps} + FPS +
+
+ {frameCount} + Frames +
+
+ )} +
+ + {/* Footer */} +
+
+ 📂 + {source} +
+
+
+ ); +}; + +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..a3cea46 --- /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, Calendar, ChevronDown, + LayoutDashboard, Camera, 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 ( +
+
+ {/* Logo Section */} +
+
+ +
+
+ Baggage Analytics Platform + Real-time monitoring & AI detection +
+
+ + {/* Navigation Links */} + + + {/* Right Section */} +
+ {/* Camera Selector */} +
setShowCameraDropdown(!showCameraDropdown)}> + + Camera 1 - Terminal A + + {showCameraDropdown && ( +
+
Camera 1 - Terminal A
+
Camera 2 - Terminal B
+
Camera 3 - Security
+
Camera 4 - Baggage Claim
+
+ )} +
+ + {/* Date Display */} +
+ Today, {today} + +
+ + {/* Dark Mode Toggle */} + + + {/* Notifications */} + + + {/* Export Report Button */} + + + {/* User Profile */} +
+ User +
+
+
+
+ ); +}; + +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 */} -
+
+ +
{children} -
+ +
+
+ © 2024 Baggage Analytics System. All systems nominal. +
+
+ + + YOLOv11 Engine Active + + Documentation + Support +
+
); } 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..a62595f --- /dev/null +++ b/frontend/src/pages/AutoAnnotationPage.jsx @@ -0,0 +1,297 @@ +import React, { useState } from "react"; +import { + Upload, AlertCircle, Loader, + Download, Trash2, Eye, 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 && ( +
+ + {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) => ( +
+
+
+ Processed +
+
+ + {item.image} + + + {item.label} + + +
+ )) + ) : ( +
+

No processed results yet. Upload images to start.

+
+ )} +
+
+ + {/* Pagination */} +
+ Page 1 of 1 +
+ + +
+
+
+
+ + {/* Footer Stats */} +
+
+ GPU UTILIZATION +
+ 76% +
+
+
+
+
+
+ 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..a731306 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, +} 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) => ( - - {project.name} - - ))} - - -
- - 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 */} +
+
+

Camera Management

+

Manage cameras across your projects

- ))} - - {/* Edit Dialog */} - setEditCamera(null)}> - Edit Camera - - setEditName(e.target.value)} - fullWidth - margin="dense" - /> - setEditUrl(e.target.value)} - fullWidth - margin="dense" - /> - - - - - - +
+ + +
+
+ +
+ {/* Left Sidebar */} + + + {/* Camera Grid */} +
+ {cameras.length > 0 ? ( +
+ {cameras.map((camera) => ( +
+
+
+ + CAMERA FEED +
+ + + AVAILABLE + +
+ +
+
+

{camera.cameraName}

+

+ + {camera.rtspUrl} +

+
+
+ +
+
+ Ready +
+ +
+
+ ))} +
+ ) : ( +
+
📹
+

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 */} +
+
+

+ Live Analytics Dashboard +

+

+ Real-time baggage detection powered by YOLO v11. Add video feeds to start analysis. +

+
+
+
+ {streams.length} + Active Feeds +
+
+
+ + {/* 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..c0e4bf3 --- /dev/null +++ b/frontend/src/pages/SettingsPage.jsx @@ -0,0 +1,233 @@ +import React, { useState } from "react"; +import { + Brain, Settings, + 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 */} +
+
+

System Settings & AI Configuration

+

Manage your AI models, detection sensitivity, and global system parameters.

+
+
+ + +
+
+ +
+ {/* 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..cf233a9 --- /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, Settings, Wifi, Play, +} 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 ? ( + Live Stream + ) : ( +
+
+ Establishing Secure Connection... +
+ )} + + {/* Baggage Count Overlay */} +
+
CURRENT COUNT
+
{currentCount}
+
+
+
+ + {/* Historical Trends Chart */} +
+
+

Historical Trends - Today

+
+ +
+
+
+ + + + + + + + + + + + + + + +
+
+ + Baggage Count +
+
+
+
+
+ + {/* 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 */} +
+
+
+ + Detection FPS +
+ {fps > 20 ? 'GOOD' : fps > 10 ? 'FAIR' : 'LOW'} +
+
+
+ {fps} + fps +
+
+
+
+
+
+ + {/* 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