commit ea9a2627b4b413d9a3b38ced1da1b48fa4162e13 Author: Alexander Borg Date: Sat Aug 16 19:43:44 2025 +0200 Initial commit diff --git a/.env.docker b/.env.docker new file mode 100644 index 0000000..fb3c56c --- /dev/null +++ b/.env.docker @@ -0,0 +1,14 @@ +# Docker Environment Configuration +# Copy this file to .env and update with your actual values + +# JWT Configuration +JWT_SECRET=your-super-secret-jwt-key-change-in-production-make-it-long-and-random + +# Twilio Configuration (for SMS alerts) +TWILIO_ACCOUNT_SID=your_twilio_account_sid_here +TWILIO_AUTH_TOKEN=your_twilio_auth_token_here +TWILIO_PHONE_NUMBER=your_twilio_phone_number_here + +# Optional: Override default settings +# NODE_ENV=production +# CORS_ORIGIN=http://localhost:3000 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..981e5c6 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,18 @@ + +- [x] Verify that the copilot-instructions.md file in the .github directory is created. ✓ + +- [ ] Clarify Project Requirements + +- [ ] Scaffold the Project + +- [ ] Customize the Project + +- [ ] Install Required Extensions + +- [ ] Compile the Project + +- [ ] Create and Run Task + +- [ ] Launch the Project + +- [ ] Ensure Documentation is Complete diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca2bfa3 --- /dev/null +++ b/README.md @@ -0,0 +1,403 @@ +# Drone Detection System + +A comprehensive real-time drone detection and monitoring system with SMS alerts, real-time mapping, and advanced analytics. + +## Features + +### Core Functionality +- **Real-time Drone Detection**: Receive and process drone detection data from hardware sensors +- **Intelligent Threat Assessment**: RSSI-based threat classification (Critical, High, Medium, Low, Monitoring) +- **Device Management**: Monitor and manage drone detection devices with heartbeat monitoring +- **Real-time Mapping**: Interactive map showing device locations and detection status with threat indicators +- **SMS Alerts**: Configurable SMS notifications via Twilio with threat-based escalation +- **Analytics Dashboard**: Real-time charts and statistics with threat level breakdowns +- **Alert Rules**: Flexible alert configuration with threat level thresholds and distance limits + +### Hardware Integration +- **Detection Data Processing**: Handles JSON data from drone detection hardware +- **Heartbeat Monitoring**: Tracks device health and connectivity status +- **Device Status Tracking**: Real-time monitoring of device online/offline status + +### Advanced Features +- **Multi-user Support**: Role-based access control (admin, operator, viewer) +- **Real-time Updates**: WebSocket-based live updates across all clients +- **Intelligent Threat Assessment**: RSSI-based distance calculation and threat classification +- **Security-Focused Alerts**: Enhanced alert messages with threat descriptions and action requirements +- **Swedish Location Support**: Pre-configured for government sites, water facilities, and sensitive areas +- **Alert Management**: Comprehensive alert rule configuration with threat level thresholds +- **Historical Data**: Complete detection history with threat level filtering and search +- **API-first Design**: RESTful API for easy integration with external systems + +## Technology Stack + +### Backend +- **Node.js** with Express.js framework +- **PostgreSQL** database with Sequelize ORM +- **Socket.IO** for real-time communication +- **Twilio** for SMS alerts +- **JWT** authentication +- **Rate limiting** and security middleware + +### Frontend +- **React 18** with modern hooks +- **React Leaflet** for interactive maps +- **Tailwind CSS** for styling +- **Recharts** for data visualization +- **Framer Motion** for animations +- **React Hot Toast** for notifications + +## Project Structure + +``` +uamils/ +├── server/ # Backend API +│ ├── models/ # Database models +│ ├── routes/ # API routes +│ ├── services/ # Business logic +│ ├── middleware/ # Express middleware +│ └── scripts/ # Database setup scripts +├── client/ # Frontend React app +│ ├── src/ +│ │ ├── components/ # Reusable components +│ │ ├── pages/ # Page components +│ │ ├── contexts/ # React contexts +│ │ └── services/ # API services +└── docs/ # Documentation +``` + +## Quick Start + +### Prerequisites +- Node.js 16+ and npm +- PostgreSQL 12+ +- Twilio account (for SMS alerts) + +### Native Installation + +1. **Clone and install dependencies**: + ```bash + git clone + cd uamils + npm run install:all + ``` + +2. **Database Setup**: + ```bash + # Create PostgreSQL database + createdb drone_detection + + # Copy environment file + cd server + cp .env.example .env + + # Edit .env with your database credentials and Twilio config + # Then run database setup + npm run db:setup + ``` + +3. **Start Development Servers**: + ```bash + # From project root - starts both backend and frontend + npm run dev + ``` + + Or start separately: + ```bash + # Backend (port 3001) + npm run server:dev + + # Frontend (port 3000) + npm run client:dev + ``` + +4. **Access the application**: + - Frontend: http://localhost:3000 + - Backend API: http://localhost:3001/api + +### Docker Installation (Recommended) + +For the easiest setup, use Docker: + +```bash +# Prerequisites: Docker and Docker Compose + +# 1. Copy environment template +cp .env.docker .env + +# 2. Edit .env with your Twilio credentials +# TWILIO_ACCOUNT_SID=your_sid +# TWILIO_AUTH_TOKEN=your_token +# TWILIO_PHONE_NUMBER=your_phone + +# 3. Start the system +docker-compose up -d + +# 4. Access the application +# Frontend: http://localhost:3000 +# Backend: http://localhost:3001/api + +# Quick start script (Windows/Linux) +./docker-start.sh # Linux/Mac +docker-start.bat # Windows +``` + +**Docker Profiles:** +- **Default**: `docker-compose up -d` (Frontend + Backend + Database + Redis) +- **Production**: `docker-compose --profile production up -d` (+ Nginx proxy) +- **Simulation**: `docker-compose --profile simulation up -d` (+ Python simulator) + +For detailed Docker deployment guide, see [docs/DOCKER_DEPLOYMENT.md](docs/DOCKER_DEPLOYMENT.md). + +## Testing with Swedish Drone Simulator + +The system includes a comprehensive Python simulation script for testing with realistic Swedish coordinates: + +```bash +# Install Python dependencies +pip install -r requirements.txt + +# Run basic simulation (5 devices at Swedish government sites) +python drone_simulator.py + +# Custom simulation with 10 devices for 2 hours +python drone_simulator.py --devices 10 --duration 7200 + +# Show available Swedish locations +python drone_simulator.py --list-locations +``` + +The simulator generates: +- **Realistic RSSI values** based on actual distance calculations +- **Threat-based scenarios** with different probability weights +- **Swedish coordinates** for government sites, water facilities, nuclear plants +- **Continuous heartbeats** for device health monitoring +- **Multiple threat levels** from monitoring (15km) to critical (<50m) + +## Threat Assessment System + +The system includes intelligent threat assessment specifically designed for Swedish government sites and sensitive facilities: + +### Threat Levels +- **🔴 CRITICAL** (0-50m): Immediate security response required +- **🟠 HIGH** (50-200m): Security response recommended +- **🟡 MEDIUM** (200m-1km): Enhanced monitoring +- **🟢 LOW** (1-5km): Standard monitoring +- **⚪ MONITORING** (5-15km): Passive monitoring + +### RSSI-Based Distance Calculation +``` +Estimated Distance = 10^((RSSI_at_1m - RSSI) / (10 * path_loss_exponent)) +``` + +### Enhanced Alert Messages +``` +🚨 SECURITY ALERT 🚨 +THREAT LEVEL: HIGH +HIGH THREAT: Drone approaching facility (50-200m) + +📍 LOCATION: Riksdag Stockholm +📏 DISTANCE: ~150m +📶 SIGNAL: -45 dBm +🚁 DRONE TYPE: Professional/Military +⚠️ IMMEDIATE ACTION REQUIRED +``` + +For detailed threat assessment documentation, see [docs/THREAT_ASSESSMENT.md](docs/THREAT_ASSESSMENT.md). + +## Configuration + +### Environment Variables (.env) + +```bash +# Database +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=drone_detection +DB_USER=postgres +DB_PASSWORD=your_password + +# JWT Secret +JWT_SECRET=your-super-secret-jwt-key + +# Twilio (for SMS alerts) +TWILIO_ACCOUNT_SID=your_twilio_sid +TWILIO_AUTH_TOKEN=your_twilio_token +TWILIO_PHONE_NUMBER=your_twilio_phone + +# API Settings +PORT=3001 +NODE_ENV=development +CORS_ORIGIN=http://localhost:3000 +``` + +## API Documentation + +### Drone Detection Endpoint +```http +POST /api/detections +Content-Type: application/json + +{ + "device_id": 1941875381, + "geo_lat": 59.3293, + "geo_lon": 18.0686, + "device_timestamp": 1691755018, + "drone_type": 0, + "rssi": -45, + "freq": 20, + "drone_id": 2 +} +``` + +### Heartbeat Endpoint +```http +POST /api/heartbeat +Content-Type: application/json + +{ + "type": "heartbeat", + "key": "device_1941875381_key", + "battery_level": 85, + "signal_strength": -50, + "temperature": 22.5 +} +``` + +### Device Management +- `GET /api/devices` - List all devices +- `GET /api/devices/map` - Get devices with location data +- `POST /api/devices` - Create new device (admin) +- `PUT /api/devices/:id` - Update device (admin) + +### User Authentication +- `POST /api/users/login` - User login +- `POST /api/users/register` - Register new user +- `GET /api/users/profile` - Get current user profile + +### Alert Management +- `GET /api/alerts/rules` - Get user's alert rules +- `POST /api/alerts/rules` - Create new alert rule +- `GET /api/alerts/logs` - Get alert history + +## Hardware Integration + +### Expected Data Format + +The system expects drone detection data in this format: +```json +{ + "device_id": 1941875381, + "geo_lat": 0, + "geo_lon": 0, + "device_timestamp": 0, + "drone_type": 0, + "rssi": 0, + "freq": 20, + "drone_id": 2 +} +``` + +### Field Descriptions +- `device_id`: Unique identifier for the detection device +- `geo_lat`/`geo_lon`: GPS coordinates of the device +- `device_timestamp`: Unix timestamp from the device +- `drone_type`: Type classification of detected drone +- `rssi`: Received Signal Strength Indicator +- `freq`: Frequency of detected signal +- `drone_id`: Unique identifier for the detected drone + +## Recommended Additional Features + +Based on the requirements, here are additional features that would enhance the system: + +### 1. **Analytics & Reporting** +- Historical trend analysis +- Drone pattern recognition +- Frequency analysis and spectrum monitoring +- Device performance metrics +- Custom report generation + +### 2. **Advanced Alerting** +- Email notifications +- Webhook integrations for external systems +- Escalation procedures +- Alert scheduling (business hours only) +- Custom alert templates + +### 3. **Security & Compliance** +- Audit logging +- Data encryption at rest +- API rate limiting per client +- GDPR compliance features +- Role-based permissions + +### 4. **Integration Features** +- REST API for third-party integrations +- Webhook support for external systems +- MQTT support for IoT devices +- Integration with security systems +- Export capabilities (CSV, PDF) + +### 5. **Advanced Mapping** +- Heatmaps of detection frequency +- Flight path tracking +- Geofencing capabilities +- Multiple map layers +- Offline map support + +### 6. **Mobile Application** +- React Native mobile app +- Push notifications +- Offline viewing capabilities +- Quick alert acknowledgment + +### 7. **AI/ML Enhancements** +- Drone behavior prediction +- False positive reduction +- Automatic drone classification +- Anomaly detection +- Pattern recognition + +## Production Deployment + +### Docker Deployment +```bash +# Build and run with Docker Compose +docker-compose up -d +``` + +### Manual Deployment +1. Set up PostgreSQL database +2. Configure environment variables +3. Build frontend: `npm run client:build` +4. Start backend: `npm run server:start` +5. Serve frontend with nginx or similar + +### Recommended Infrastructure +- **Database**: PostgreSQL with read replicas +- **Caching**: Redis for session management +- **Load Balancer**: nginx or AWS ALB +- **Monitoring**: Prometheus + Grafana +- **Logging**: ELK stack or similar + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make changes with tests +4. Submit a pull request + +## License + +[Add your license here] + +## Support + +For support and questions: +- Create an issue in the repository +- Contact the development team +- Check the documentation in `/docs` + +--- + +**Note**: This system is designed for security and monitoring purposes. Ensure compliance with local regulations regarding drone detection and monitoring. diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..5054b86 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,210 @@ +# Quick Setup Guide + +This guide will help you get the Drone Detection System up and running quickly. + +## Prerequisites + +1. **Node.js 16+** and npm +2. **PostgreSQL 12+** +3. **Twilio Account** (for SMS alerts) + +## Step-by-Step Setup + +### 1. Install Dependencies + +```bash +# Install all dependencies (root, server, and client) +npm run install:all +``` + +### 2. Database Setup + +#### Option A: Automatic Setup (Recommended) +```bash +# Make sure PostgreSQL is running +# Create database +createdb drone_detection + +# Copy environment file and configure +cd server +cp .env.example .env +# Edit .env with your database credentials and Twilio settings + +# Run automated setup +cd .. +npm run db:setup +``` + +#### Option B: Manual Setup +```bash +# Create PostgreSQL database +createdb drone_detection + +# Configure environment +cd server +cp .env.example .env +# Edit the .env file with your settings + +# Run database setup script +node scripts/setup-database.js +``` + +### 3. Environment Configuration + +Edit `server/.env` with your settings: + +```bash +# Database +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=drone_detection +DB_USER=postgres +DB_PASSWORD=your_password + +# JWT Secret (generate a random string) +JWT_SECRET=your-super-secret-jwt-key + +# Twilio (get from https://console.twilio.com/) +TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxx +TWILIO_AUTH_TOKEN=your_auth_token +TWILIO_PHONE_NUMBER=+1234567890 + +# Server +PORT=3001 +NODE_ENV=development +CORS_ORIGIN=http://localhost:3000 +``` + +### 4. Start the Application + +```bash +# Start both backend and frontend in development mode +npm run dev +``` + +Or start separately: +```bash +# Start backend (port 3001) +npm run server:dev + +# Start frontend (port 3000) +npm run client:dev +``` + +### 5. Access the Application + +- **Frontend**: http://localhost:3000 +- **Backend API**: http://localhost:3001/api + +### 6. Default Login Credentials + +- **Admin**: `admin` / `admin123` +- **Operator**: `operator` / `operator123` + +## Testing the System + +### 1. Test Drone Detection API + +```bash +curl -X POST http://localhost:3001/api/detections \ + -H "Content-Type: application/json" \ + -d '{ + "device_id": 1941875381, + "geo_lat": 59.3293, + "geo_lon": 18.0686, + "device_timestamp": 1691755018, + "drone_type": 0, + "rssi": -45, + "freq": 20, + "drone_id": 2 + }' +``` + +### 2. Test Heartbeat API + +```bash +curl -X POST http://localhost:3001/api/heartbeat \ + -H "Content-Type: application/json" \ + -d '{ + "type": "heartbeat", + "key": "device_1941875381_key", + "battery_level": 85, + "signal_strength": -50, + "temperature": 22.5 + }' +``` + +## Common Issues + +### Database Connection Issues +- Ensure PostgreSQL is running +- Check database credentials in `.env` +- Verify database exists: `psql -l` + +### Port Already in Use +```bash +# Find process using port 3001 +netstat -ano | findstr :3001 +# Kill process (Windows) +taskkill /PID /F +``` + +### Missing Dependencies +```bash +# Reinstall all dependencies +npm run install:all +``` + +### Twilio SMS Not Working +- Verify Twilio credentials in `.env` +- Check phone number format (+1234567890) +- Ensure Twilio account has sufficient credits + +## Docker Setup (Alternative) + +If you prefer Docker: + +```bash +# Copy environment file +cp server/.env.example server/.env +# Edit server/.env with your settings + +# Build and start with Docker +npm run docker:up + +# Stop services +npm run docker:down +``` + +## Production Deployment + +For production deployment: + +1. Set `NODE_ENV=production` in server/.env +2. Build frontend: `npm run client:build` +3. Use a reverse proxy (nginx) for the frontend +4. Use PM2 or similar for process management +5. Set up SSL certificates +6. Configure proper database backup + +## Next Steps + +1. **Configure Alert Rules**: Log in and set up SMS alert rules +2. **Add Devices**: Register your drone detection hardware +3. **Test Real-time Features**: Check the dashboard for live updates +4. **Customize Settings**: Modify alert conditions and user roles + +## Getting Help + +- Check the main README.md for detailed documentation +- Review the API endpoints in the backend routes +- Check browser console for frontend issues +- Review server logs for backend issues + +## Security Notes + +- Change default passwords immediately +- Use strong JWT secrets in production +- Enable HTTPS in production +- Regularly update dependencies +- Monitor access logs diff --git a/client/.dockerignore b/client/.dockerignore new file mode 100644 index 0000000..e763aaa --- /dev/null +++ b/client/.dockerignore @@ -0,0 +1,76 @@ +# Dependencies +node_modules +/.pnp +.pnp.js + +# Testing +/coverage + +# Production build +/build +/dist + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Git +.git +.gitignore + +# Docker +Dockerfile* +.dockerignore + +# Documentation +README.md +*.md + +# ESLint +.eslintcache + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 0000000..2f21192 --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,54 @@ +# Frontend Dockerfile for Drone Detection System +# Multi-stage build for optimized production image + +# Build stage +FROM node:18-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies (including dev dependencies for build) +RUN npm ci + +# Copy source code +COPY . . + +# Build arguments for environment variables +ARG VITE_API_URL=http://localhost:3001/api +ARG VITE_WS_URL=ws://localhost:3001 + +# Set environment variables for build +ENV VITE_API_URL=$VITE_API_URL +ENV VITE_WS_URL=$VITE_WS_URL + +# Build the application +RUN npm run build + +# Production stage +FROM nginx:alpine AS production + +# Install curl for health checks +RUN apk add --no-cache curl + +# Copy built application from builder stage +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy custom nginx configuration +COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf + +# Create nginx user and set permissions +RUN chown -R nginx:nginx /usr/share/nginx/html && \ + chmod -R 755 /usr/share/nginx/html + +# Expose port 80 +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD curl -f http://localhost:80 || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..ad539ba --- /dev/null +++ b/client/index.html @@ -0,0 +1,13 @@ + + + + + + Drone Detection System + + + +
+ + + diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..af8c61c --- /dev/null +++ b/client/package.json @@ -0,0 +1,50 @@ +{ + "name": "drone-detection-client", + "version": "1.0.0", + "description": "Frontend for drone detection system", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.15.0", + "react-leaflet": "^4.2.1", + "leaflet": "^1.9.4", + "socket.io-client": "^4.7.2", + "axios": "^1.5.0", + "recharts": "^2.8.0", + "@headlessui/react": "^1.7.17", + "@heroicons/react": "^2.0.18", + "tailwindcss": "^3.3.3", + "date-fns": "^2.30.0", + "react-hot-toast": "^2.4.1", + "framer-motion": "^10.16.4", + "classnames": "^2.3.2" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.0.3", + "vite": "^4.4.5", + "autoprefixer": "^10.4.15", + "postcss": "^8.4.29", + "eslint": "^8.45.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "@types/leaflet": "^1.9.4" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/client/postcss.config.js b/client/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/client/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/client/src/App.jsx b/client/src/App.jsx new file mode 100644 index 0000000..61d9ac9 --- /dev/null +++ b/client/src/App.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import { Toaster } from 'react-hot-toast'; +import { AuthProvider } from './contexts/AuthContext'; +import { SocketProvider } from './contexts/SocketContext'; +import Layout from './components/Layout'; +import Dashboard from './pages/Dashboard'; +import MapView from './pages/MapView'; +import Devices from './pages/Devices'; +import Detections from './pages/Detections'; +import Alerts from './pages/Alerts'; +import Login from './pages/Login'; +import ProtectedRoute from './components/ProtectedRoute'; + +function App() { + return ( + + + +
+ + + + } /> + + + + + }> + } /> + } /> + } /> + } /> + } /> + + +
+
+
+
+ ); +} + +export default App; diff --git a/client/src/components/Layout.jsx b/client/src/components/Layout.jsx new file mode 100644 index 0000000..68d65eb --- /dev/null +++ b/client/src/components/Layout.jsx @@ -0,0 +1,190 @@ +import React, { useState } from 'react'; +import { Outlet, Link, useLocation } from 'react-router-dom'; +import { useAuth } from '../contexts/AuthContext'; +import { useSocket } from '../contexts/SocketContext'; +import { + HomeIcon, + MapIcon, + ServerIcon, + ExclamationTriangleIcon, + BellIcon, + UserIcon, + Bars3Icon, + XMarkIcon, + SignalIcon, + WifiIcon +} from '@heroicons/react/24/outline'; +import classNames from 'classnames'; + +const navigation = [ + { name: 'Dashboard', href: '/', icon: HomeIcon }, + { name: 'Map View', href: '/map', icon: MapIcon }, + { name: 'Devices', href: '/devices', icon: ServerIcon }, + { name: 'Detections', href: '/detections', icon: ExclamationTriangleIcon }, + { name: 'Alerts', href: '/alerts', icon: BellIcon }, +]; + +const Layout = () => { + const [sidebarOpen, setSidebarOpen] = useState(false); + const { user, logout } = useAuth(); + const { connected, recentDetections } = useSocket(); + const location = useLocation(); + + return ( +
+ {/* Mobile sidebar */} +
+
setSidebarOpen(false)} /> +
+
+ +
+ +
+
+ + {/* Static sidebar for desktop */} +
+
+
+ +
+
+
+ + {/* Main content */} +
+
+ + + {/* Top navigation */} +
+
+

+ {navigation.find(item => item.href === location.pathname)?.name || 'Drone Detection System'} +

+
+ +
+ {/* Connection status */} +
+
+ {connected ? ( + + ) : ( + + )} + {connected ? 'Connected' : 'Disconnected'} +
+
+ + {/* Recent detections count */} + {recentDetections.length > 0 && ( +
+ + {recentDetections.length} recent +
+ )} + + {/* User menu */} +
+
+
+ + {user?.username} +
+ +
+
+
+
+
+ + {/* Page content */} +
+
+
+ +
+
+
+
+
+ ); +}; + +const SidebarContent = () => { + const location = useLocation(); + + return ( + <> +
+
+
+ +
+

+ Drone Detector +

+
+
+ +
+ +
+ + ); +}; + +export default Layout; diff --git a/client/src/components/ProtectedRoute.jsx b/client/src/components/ProtectedRoute.jsx new file mode 100644 index 0000000..f6b5cde --- /dev/null +++ b/client/src/components/ProtectedRoute.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Navigate } from 'react-router-dom'; +import { useAuth } from '../contexts/AuthContext'; + +const ProtectedRoute = ({ children }) => { + const { isAuthenticated, loading } = useAuth(); + + if (loading) { + return ( +
+
+
+ ); + } + + if (!isAuthenticated) { + return ; + } + + return children; +}; + +export default ProtectedRoute; diff --git a/client/src/contexts/AuthContext.jsx b/client/src/contexts/AuthContext.jsx new file mode 100644 index 0000000..c6d4a27 --- /dev/null +++ b/client/src/contexts/AuthContext.jsx @@ -0,0 +1,136 @@ +import React, { createContext, useContext, useReducer, useEffect } from 'react'; +import api from '../services/api'; + +const AuthContext = createContext(); + +const authReducer = (state, action) => { + switch (action.type) { + case 'LOGIN_START': + return { ...state, loading: true, error: null }; + case 'LOGIN_SUCCESS': + return { + ...state, + loading: false, + user: action.payload.user, + token: action.payload.token, + isAuthenticated: true + }; + case 'LOGIN_FAILURE': + return { + ...state, + loading: false, + error: action.payload, + user: null, + token: null, + isAuthenticated: false + }; + case 'LOGOUT': + return { + ...state, + user: null, + token: null, + isAuthenticated: false, + loading: false, + error: null + }; + case 'SET_LOADING': + return { ...state, loading: action.payload }; + case 'SET_ERROR': + return { ...state, error: action.payload }; + case 'UPDATE_USER': + return { ...state, user: { ...state.user, ...action.payload } }; + default: + return state; + } +}; + +const initialState = { + user: null, + token: localStorage.getItem('token'), + isAuthenticated: false, + loading: true, + error: null +}; + +export const AuthProvider = ({ children }) => { + const [state, dispatch] = useReducer(authReducer, initialState); + + useEffect(() => { + // Check if user is logged in on app start + const token = localStorage.getItem('token'); + if (token) { + // Validate token and get user info + checkAuthStatus(); + } else { + dispatch({ type: 'SET_LOADING', payload: false }); + } + }, []); + + const checkAuthStatus = async () => { + try { + const response = await api.get('/users/profile'); + dispatch({ + type: 'LOGIN_SUCCESS', + payload: { + user: response.data.data, + token: localStorage.getItem('token') + } + }); + } catch (error) { + localStorage.removeItem('token'); + dispatch({ type: 'LOGOUT' }); + } + }; + + const login = async (credentials) => { + try { + dispatch({ type: 'LOGIN_START' }); + const response = await api.post('/users/login', credentials); + + const { user, token } = response.data.data; + localStorage.setItem('token', token); + + dispatch({ + type: 'LOGIN_SUCCESS', + payload: { user, token } + }); + + return { success: true }; + } catch (error) { + const errorMessage = error.response?.data?.message || 'Login failed'; + dispatch({ type: 'LOGIN_FAILURE', payload: errorMessage }); + return { success: false, error: errorMessage }; + } + }; + + const logout = () => { + localStorage.removeItem('token'); + dispatch({ type: 'LOGOUT' }); + }; + + const updateUser = (userData) => { + dispatch({ type: 'UPDATE_USER', payload: userData }); + }; + + const value = { + ...state, + login, + logout, + updateUser, + checkAuthStatus + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; diff --git a/client/src/contexts/SocketContext.jsx b/client/src/contexts/SocketContext.jsx new file mode 100644 index 0000000..ed53412 --- /dev/null +++ b/client/src/contexts/SocketContext.jsx @@ -0,0 +1,134 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { io } from 'socket.io-client'; +import { useAuth } from './AuthContext'; +import toast from 'react-hot-toast'; + +const SocketContext = createContext(); + +export const SocketProvider = ({ children }) => { + const [socket, setSocket] = useState(null); + const [connected, setConnected] = useState(false); + const [recentDetections, setRecentDetections] = useState([]); + const [deviceStatus, setDeviceStatus] = useState({}); + const { isAuthenticated } = useAuth(); + + useEffect(() => { + if (isAuthenticated) { + // Initialize socket connection + const newSocket = io(process.env.NODE_ENV === 'production' + ? window.location.origin + : 'http://localhost:3001' + ); + + newSocket.on('connect', () => { + console.log('Connected to server'); + setConnected(true); + + // Join dashboard room for general updates + newSocket.emit('join_dashboard'); + + toast.success('Connected to real-time updates'); + }); + + newSocket.on('disconnect', () => { + console.log('Disconnected from server'); + setConnected(false); + toast.error('Disconnected from server'); + }); + + newSocket.on('connect_error', (error) => { + console.error('Connection error:', error); + setConnected(false); + toast.error('Failed to connect to server'); + }); + + // Listen for drone detections + newSocket.on('drone_detection', (detection) => { + console.log('New drone detection:', detection); + + setRecentDetections(prev => [detection, ...prev.slice(0, 49)]); // Keep last 50 + + // Show toast notification + toast.error( + `Drone detected by ${detection.device.name || `Device ${detection.device_id}`}`, + { + duration: 5000, + icon: '🚨', + } + ); + }); + + // Listen for device heartbeats + newSocket.on('device_heartbeat', (heartbeat) => { + console.log('Device heartbeat:', heartbeat); + + setDeviceStatus(prev => ({ + ...prev, + [heartbeat.device_id]: { + ...heartbeat, + last_seen: new Date() + } + })); + }); + + // Listen for device updates + newSocket.on('device_updated', (device) => { + console.log('Device updated:', device); + toast.success(`Device ${device.name || device.id} updated`); + }); + + setSocket(newSocket); + + return () => { + newSocket.disconnect(); + }; + } else { + // Disconnect if not authenticated + if (socket) { + socket.disconnect(); + setSocket(null); + setConnected(false); + } + } + }, [isAuthenticated]); + + const joinDeviceRoom = (deviceId) => { + if (socket) { + socket.emit('join_device_room', deviceId); + } + }; + + const leaveDeviceRoom = (deviceId) => { + if (socket) { + socket.emit('leave_device_room', deviceId); + } + }; + + const clearRecentDetections = () => { + setRecentDetections([]); + }; + + const value = { + socket, + connected, + recentDetections, + deviceStatus, + joinDeviceRoom, + leaveDeviceRoom, + clearRecentDetections + }; + + return ( + + {children} + + ); +}; + +export const useSocket = () => { + const context = useContext(SocketContext); + if (!context) { + throw new Error('useSocket must be used within a SocketProvider'); + } + return context; +}; diff --git a/client/src/index.css b/client/src/index.css new file mode 100644 index 0000000..d56e16f --- /dev/null +++ b/client/src/index.css @@ -0,0 +1,103 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Custom styles */ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Leaflet map fixes */ +.leaflet-container { + height: 100%; + width: 100%; +} + +.leaflet-popup-content { + font-family: inherit; +} + +/* Custom components */ +.card { + @apply bg-white rounded-lg shadow-md border border-gray-200; +} + +.card-header { + @apply px-6 py-4 border-b border-gray-200; +} + +.card-body { + @apply px-6 py-4; +} + +.btn { + @apply px-4 py-2 rounded-md font-medium transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2; +} + +.btn-primary { + @apply bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500; +} + +.btn-secondary { + @apply bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500; +} + +.btn-danger { + @apply bg-danger-600 text-white hover:bg-danger-700 focus:ring-danger-500; +} + +.btn-success { + @apply bg-success-600 text-white hover:bg-success-700 focus:ring-success-500; +} + +.status-indicator { + @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium; +} + +.status-online { + @apply bg-success-100 text-success-800; +} + +.status-offline { + @apply bg-danger-100 text-danger-800; +} + +.status-warning { + @apply bg-warning-100 text-warning-800; +} + +/* Animation classes */ +.fade-in { + @apply animate-fade-in; +} + +.slide-up { + @apply animate-slide-up; +} + +/* Map marker pulse animation */ +.marker-pulse { + @apply animate-pulse-slow; +} + +/* Responsive table */ +.table-responsive { + @apply overflow-x-auto; +} + +.table { + @apply min-w-full divide-y divide-gray-200; +} + +.table th { + @apply px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider; +} + +.table td { + @apply px-6 py-4 whitespace-nowrap text-sm text-gray-900; +} diff --git a/client/src/main.jsx b/client/src/main.jsx new file mode 100644 index 0000000..54b39dd --- /dev/null +++ b/client/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/client/src/pages/Alerts.jsx b/client/src/pages/Alerts.jsx new file mode 100644 index 0000000..40ef546 --- /dev/null +++ b/client/src/pages/Alerts.jsx @@ -0,0 +1,574 @@ +import React, { useState, useEffect } from 'react'; +import api from '../services/api'; +import { format } from 'date-fns'; +import { + PlusIcon, + BellIcon, + CheckCircleIcon, + XCircleIcon, + ExclamationTriangleIcon +} from '@heroicons/react/24/outline'; + +const Alerts = () => { + const [alertRules, setAlertRules] = useState([]); + const [alertLogs, setAlertLogs] = useState([]); + const [alertStats, setAlertStats] = useState(null); + const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState('rules'); + const [showCreateModal, setShowCreateModal] = useState(false); + + useEffect(() => { + fetchAlertData(); + }, []); + + const fetchAlertData = async () => { + try { + const [rulesRes, logsRes, statsRes] = await Promise.all([ + api.get('/alerts/rules'), + api.get('/alerts/logs?limit=50'), + api.get('/alerts/stats?hours=24') + ]); + + setAlertRules(rulesRes.data.data); + setAlertLogs(logsRes.data.data); + setAlertStats(statsRes.data.data); + } catch (error) { + console.error('Error fetching alert data:', error); + } finally { + setLoading(false); + } + }; + + const handleDeleteRule = async (ruleId) => { + if (window.confirm('Are you sure you want to delete this alert rule?')) { + try { + await api.delete(`/alerts/rules/${ruleId}`); + fetchAlertData(); + } catch (error) { + console.error('Error deleting alert rule:', error); + } + } + }; + + const getStatusIcon = (status) => { + switch (status) { + case 'sent': + return ; + case 'failed': + return ; + case 'pending': + return ; + default: + return ; + } + }; + + const getPriorityColor = (priority) => { + switch (priority) { + case 'critical': + return 'bg-red-100 text-red-800'; + case 'high': + return 'bg-orange-100 text-orange-800'; + case 'medium': + return 'bg-yellow-100 text-yellow-800'; + case 'low': + return 'bg-green-100 text-green-800'; + default: + return 'bg-gray-100 text-gray-800'; + } + }; + + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+
+
+

+ Alert Management +

+

+ Configure and monitor alert rules for drone detections +

+
+ +
+ + {/* Alert Stats */} + {alertStats && ( +
+
+
{alertStats.total_alerts}
+
Total Alerts (24h)
+
+
+
{alertStats.sent_alerts}
+
Sent Successfully
+
+
+
{alertStats.failed_alerts}
+
Failed
+
+
+
{alertStats.pending_alerts}
+
Pending
+
+
+ )} + + {/* Tabs */} +
+ +
+ + {/* Alert Rules Tab */} + {activeTab === 'rules' && ( +
+ {alertRules.length === 0 ? ( +
+ +

No alert rules

+

+ Get started by creating your first alert rule. +

+
+ +
+
+ ) : ( +
+ + + + + + + + + + + + + + {alertRules.map((rule) => ( + + + + + + + + + + ))} + +
NamePriorityChannelsConditionsStatusCreatedActions
+
+
+ {rule.name} +
+ {rule.description && ( +
+ {rule.description} +
+ )} +
+
+ + {rule.priority} + + +
+ {rule.alert_channels.map((channel, index) => ( + + {channel} + + ))} +
+
+
+ {rule.min_detections > 1 && ( +
Min detections: {rule.min_detections}
+ )} + {rule.time_window && ( +
Time window: {rule.time_window}s
+ )} + {rule.cooldown_period && ( +
Cooldown: {rule.cooldown_period}s
+ )} +
+
+ + {rule.is_active ? 'Active' : 'Inactive'} + + +
+ {format(new Date(rule.created_at), 'MMM dd, yyyy')} +
+
+
+ + +
+
+
+ )} +
+ )} + + {/* Alert Logs Tab */} + {activeTab === 'logs' && ( +
+ {alertLogs.length === 0 ? ( +
+ +

No alert logs

+

+ Alert logs will appear here when alerts are triggered. +

+
+ ) : ( +
+ + + + + + + + + + + + + {alertLogs.map((log) => ( + + + + + + + + + ))} + +
StatusTypeRecipientRuleMessageSent At
+
+ {getStatusIcon(log.status)} + + {log.status} + +
+
+ + {log.alert_type} + + +
+ {log.recipient} +
+
+
+ {log.rule?.name || 'Unknown Rule'} +
+
+
+ {log.message} +
+
+
+ {log.sent_at + ? format(new Date(log.sent_at), 'MMM dd, HH:mm') + : 'Not sent' + } +
+
+
+ )} +
+ )} + + {/* Create Alert Rule Modal */} + {showCreateModal && ( + setShowCreateModal(false)} + onSave={() => { + setShowCreateModal(false); + fetchAlertData(); + }} + /> + )} +
+ ); +}; + +const CreateAlertRuleModal = ({ onClose, onSave }) => { + const [formData, setFormData] = useState({ + name: '', + description: '', + priority: 'medium', + alert_channels: ['sms'], + min_detections: 1, + time_window: 300, + cooldown_period: 600, + device_ids: null, + drone_types: null, + min_rssi: '', + max_rssi: '', + frequency_ranges: [] + }); + const [saving, setSaving] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + setSaving(true); + + try { + const payload = { ...formData }; + + // Clean up empty values + if (!payload.min_rssi) delete payload.min_rssi; + if (!payload.max_rssi) delete payload.max_rssi; + if (!payload.device_ids || payload.device_ids.length === 0) payload.device_ids = null; + if (!payload.drone_types || payload.drone_types.length === 0) payload.drone_types = null; + if (!payload.frequency_ranges || payload.frequency_ranges.length === 0) payload.frequency_ranges = null; + + await api.post('/alerts/rules', payload); + onSave(); + } catch (error) { + console.error('Error creating alert rule:', error); + } finally { + setSaving(false); + } + }; + + const handleChange = (e) => { + const { name, value, type } = e.target; + setFormData(prev => ({ + ...prev, + [name]: type === 'number' ? parseInt(value) || 0 : value + })); + }; + + const handleChannelChange = (channel, checked) => { + setFormData(prev => ({ + ...prev, + alert_channels: checked + ? [...prev.alert_channels, channel] + : prev.alert_channels.filter(c => c !== channel) + })); + }; + + return ( +
+
+
+ +
+
+
+
+

+ Create Alert Rule +

+
+ +
+
+ + +
+ +
+ +