Initial commit

This commit is contained in:
2025-08-16 19:43:44 +02:00
commit ea9a2627b4
64 changed files with 9232 additions and 0 deletions

14
.env.docker Normal file
View File

@@ -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

18
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,18 @@
<!-- Use this file to provide workspace-specific custom instructions to Copilot. For more details, visit https://code.visualstudio.com/docs/copilot/copilot-customization#_use-a-githubcopilotinstructionsmd-file -->
- [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

403
README.md Normal file
View File

@@ -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 <repository-url>
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.

210
SETUP.md Normal file
View File

@@ -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 <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

76
client/.dockerignore Normal file
View File

@@ -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

54
client/Dockerfile Normal file
View File

@@ -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;"]

13
client/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Drone Detection System</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

50
client/package.json Normal file
View File

@@ -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"
]
}
}

6
client/postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

68
client/src/App.jsx Normal file
View File

@@ -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 (
<AuthProvider>
<SocketProvider>
<Router>
<div className="App">
<Toaster
position="top-right"
toastOptions={{
duration: 4000,
style: {
background: '#363636',
color: '#fff',
},
success: {
duration: 3000,
iconTheme: {
primary: '#4ade80',
secondary: '#fff',
},
},
error: {
duration: 5000,
iconTheme: {
primary: '#ef4444',
secondary: '#fff',
},
},
}}
/>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={
<ProtectedRoute>
<Layout />
</ProtectedRoute>
}>
<Route index element={<Dashboard />} />
<Route path="map" element={<MapView />} />
<Route path="devices" element={<Devices />} />
<Route path="detections" element={<Detections />} />
<Route path="alerts" element={<Alerts />} />
</Route>
</Routes>
</div>
</Router>
</SocketProvider>
</AuthProvider>
);
}
export default App;

View File

@@ -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 (
<div className="h-screen flex overflow-hidden bg-gray-100">
{/* Mobile sidebar */}
<div className={classNames(
'fixed inset-0 flex z-40 md:hidden',
sidebarOpen ? 'block' : 'hidden'
)}>
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" onClick={() => setSidebarOpen(false)} />
<div className="relative flex-1 flex flex-col max-w-xs w-full pt-5 pb-4 bg-white">
<div className="absolute top-0 right-0 -mr-12 pt-2">
<button
type="button"
className="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
onClick={() => setSidebarOpen(false)}
>
<XMarkIcon className="h-6 w-6 text-white" />
</button>
</div>
<SidebarContent />
</div>
</div>
{/* Static sidebar for desktop */}
<div className="hidden md:flex md:flex-shrink-0">
<div className="flex flex-col w-64">
<div className="flex flex-col flex-grow pt-5 pb-4 overflow-y-auto bg-white border-r border-gray-200">
<SidebarContent />
</div>
</div>
</div>
{/* Main content */}
<div className="flex-1 overflow-hidden">
<div className="relative z-10 flex-shrink-0 flex h-16 bg-white border-b border-gray-200">
<button
type="button"
className="px-4 border-r border-gray-200 text-gray-400 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500 md:hidden"
onClick={() => setSidebarOpen(true)}
>
<Bars3Icon className="h-6 w-6" />
</button>
{/* Top navigation */}
<div className="flex-1 px-4 flex justify-between items-center">
<div className="flex-1 flex">
<h1 className="text-xl font-semibold text-gray-900">
{navigation.find(item => item.href === location.pathname)?.name || 'Drone Detection System'}
</h1>
</div>
<div className="ml-4 flex items-center md:ml-6 space-x-4">
{/* Connection status */}
<div className="flex items-center space-x-2">
<div className={classNames(
'flex items-center space-x-1 px-2 py-1 rounded-full text-xs font-medium',
connected
? 'bg-success-100 text-success-800'
: 'bg-danger-100 text-danger-800'
)}>
{connected ? (
<WifiIcon className="h-3 w-3" />
) : (
<SignalIcon className="h-3 w-3" />
)}
<span>{connected ? 'Connected' : 'Disconnected'}</span>
</div>
</div>
{/* Recent detections count */}
{recentDetections.length > 0 && (
<div className="flex items-center space-x-1 px-2 py-1 bg-danger-100 text-danger-800 rounded-full text-xs font-medium">
<ExclamationTriangleIcon className="h-3 w-3" />
<span>{recentDetections.length} recent</span>
</div>
)}
{/* User menu */}
<div className="ml-3 relative">
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-1 text-sm text-gray-700">
<UserIcon className="h-4 w-4" />
<span>{user?.username}</span>
</div>
<button
onClick={logout}
className="text-sm text-gray-500 hover:text-gray-700"
>
Logout
</button>
</div>
</div>
</div>
</div>
</div>
{/* Page content */}
<main className="flex-1 relative overflow-y-auto focus:outline-none">
<div className="py-6">
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
<Outlet />
</div>
</div>
</main>
</div>
</div>
);
};
const SidebarContent = () => {
const location = useLocation();
return (
<>
<div className="flex items-center flex-shrink-0 px-4">
<div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center">
<ExclamationTriangleIcon className="h-5 w-5 text-white" />
</div>
<h1 className="text-lg font-bold text-gray-900">
Drone Detector
</h1>
</div>
</div>
<div className="mt-5 flex-grow flex flex-col">
<nav className="flex-1 px-2 space-y-1">
{navigation.map((item) => {
const isActive = location.pathname === item.href;
return (
<Link
key={item.name}
to={item.href}
className={classNames(
isActive
? 'bg-primary-100 border-primary-500 text-primary-700'
: 'border-transparent text-gray-600 hover:bg-gray-50 hover:text-gray-900',
'group flex items-center px-2 py-2 text-sm font-medium border-l-4 transition-colors duration-200'
)}
>
<item.icon
className={classNames(
isActive
? 'text-primary-500'
: 'text-gray-400 group-hover:text-gray-500',
'mr-3 h-5 w-5'
)}
/>
{item.name}
</Link>
);
})}
</nav>
</div>
</>
);
};
export default Layout;

View File

@@ -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 (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary-600"></div>
</div>
);
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return children;
};
export default ProtectedRoute;

View File

@@ -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 (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View File

@@ -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 (
<SocketContext.Provider value={value}>
{children}
</SocketContext.Provider>
);
};
export const useSocket = () => {
const context = useContext(SocketContext);
if (!context) {
throw new Error('useSocket must be used within a SocketProvider');
}
return context;
};

103
client/src/index.css Normal file
View File

@@ -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;
}

10
client/src/main.jsx Normal file
View File

@@ -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(
<React.StrictMode>
<App />
</React.StrictMode>,
)

574
client/src/pages/Alerts.jsx Normal file
View File

@@ -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 <CheckCircleIcon className="h-5 w-5 text-green-500" />;
case 'failed':
return <XCircleIcon className="h-5 w-5 text-red-500" />;
case 'pending':
return <ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />;
default:
return <BellIcon className="h-5 w-5 text-gray-500" />;
}
};
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 (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary-600"></div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">
Alert Management
</h3>
<p className="mt-1 text-sm text-gray-500">
Configure and monitor alert rules for drone detections
</p>
</div>
<button
onClick={() => setShowCreateModal(true)}
className="btn btn-primary flex items-center space-x-2"
>
<PlusIcon className="h-4 w-4" />
<span>Create Alert Rule</span>
</button>
</div>
{/* Alert Stats */}
{alertStats && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white p-4 rounded-lg shadow border">
<div className="text-2xl font-bold text-gray-900">{alertStats.total_alerts}</div>
<div className="text-sm text-gray-500">Total Alerts (24h)</div>
</div>
<div className="bg-white p-4 rounded-lg shadow border">
<div className="text-2xl font-bold text-green-600">{alertStats.sent_alerts}</div>
<div className="text-sm text-gray-500">Sent Successfully</div>
</div>
<div className="bg-white p-4 rounded-lg shadow border">
<div className="text-2xl font-bold text-red-600">{alertStats.failed_alerts}</div>
<div className="text-sm text-gray-500">Failed</div>
</div>
<div className="bg-white p-4 rounded-lg shadow border">
<div className="text-2xl font-bold text-yellow-600">{alertStats.pending_alerts}</div>
<div className="text-sm text-gray-500">Pending</div>
</div>
</div>
)}
{/* Tabs */}
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('rules')}
className={`whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'rules'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Alert Rules ({alertRules.length})
</button>
<button
onClick={() => setActiveTab('logs')}
className={`whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'logs'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Alert Logs ({alertLogs.length})
</button>
</nav>
</div>
{/* Alert Rules Tab */}
{activeTab === 'rules' && (
<div className="bg-white rounded-lg shadow overflow-hidden">
{alertRules.length === 0 ? (
<div className="text-center py-12">
<BellIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No alert rules</h3>
<p className="mt-1 text-sm text-gray-500">
Get started by creating your first alert rule.
</p>
<div className="mt-6">
<button
onClick={() => setShowCreateModal(true)}
className="btn btn-primary"
>
<PlusIcon className="h-4 w-4 mr-2" />
Create Alert Rule
</button>
</div>
</div>
) : (
<div className="table-responsive">
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Priority</th>
<th>Channels</th>
<th>Conditions</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{alertRules.map((rule) => (
<tr key={rule.id} className="hover:bg-gray-50">
<td>
<div>
<div className="text-sm font-medium text-gray-900">
{rule.name}
</div>
{rule.description && (
<div className="text-sm text-gray-500">
{rule.description}
</div>
)}
</div>
</td>
<td>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
getPriorityColor(rule.priority)
}`}>
{rule.priority}
</span>
</td>
<td>
<div className="flex space-x-1">
{rule.alert_channels.map((channel, index) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs"
>
{channel}
</span>
))}
</div>
</td>
<td>
<div className="text-sm text-gray-900">
{rule.min_detections > 1 && (
<div>Min detections: {rule.min_detections}</div>
)}
{rule.time_window && (
<div>Time window: {rule.time_window}s</div>
)}
{rule.cooldown_period && (
<div>Cooldown: {rule.cooldown_period}s</div>
)}
</div>
</td>
<td>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
rule.is_active
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}>
{rule.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div className="text-sm text-gray-900">
{format(new Date(rule.created_at), 'MMM dd, yyyy')}
</div>
</td>
<td>
<div className="flex space-x-2">
<button
onClick={() => {
// TODO: Edit rule
console.log('Edit rule:', rule);
}}
className="text-primary-600 hover:text-primary-900 text-sm"
>
Edit
</button>
<button
onClick={() => handleDeleteRule(rule.id)}
className="text-red-600 hover:text-red-900 text-sm"
>
Delete
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{/* Alert Logs Tab */}
{activeTab === 'logs' && (
<div className="bg-white rounded-lg shadow overflow-hidden">
{alertLogs.length === 0 ? (
<div className="text-center py-12">
<BellIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No alert logs</h3>
<p className="mt-1 text-sm text-gray-500">
Alert logs will appear here when alerts are triggered.
</p>
</div>
) : (
<div className="table-responsive">
<table className="table">
<thead>
<tr>
<th>Status</th>
<th>Type</th>
<th>Recipient</th>
<th>Rule</th>
<th>Message</th>
<th>Sent At</th>
</tr>
</thead>
<tbody>
{alertLogs.map((log) => (
<tr key={log.id} className="hover:bg-gray-50">
<td>
<div className="flex items-center space-x-2">
{getStatusIcon(log.status)}
<span className="text-sm text-gray-900 capitalize">
{log.status}
</span>
</div>
</td>
<td>
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">
{log.alert_type}
</span>
</td>
<td>
<div className="text-sm text-gray-900">
{log.recipient}
</div>
</td>
<td>
<div className="text-sm text-gray-900">
{log.rule?.name || 'Unknown Rule'}
</div>
</td>
<td>
<div className="text-sm text-gray-900 max-w-xs truncate">
{log.message}
</div>
</td>
<td>
<div className="text-sm text-gray-900">
{log.sent_at
? format(new Date(log.sent_at), 'MMM dd, HH:mm')
: 'Not sent'
}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{/* Create Alert Rule Modal */}
{showCreateModal && (
<CreateAlertRuleModal
onClose={() => setShowCreateModal(false)}
onSave={() => {
setShowCreateModal(false);
fetchAlertData();
}}
/>
)}
</div>
);
};
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 (
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onClick={onClose} />
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full">
<form onSubmit={handleSubmit}>
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="mb-4">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Create Alert Rule
</h3>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Rule Name *
</label>
<input
type="text"
name="name"
required
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.name}
onChange={handleChange}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
name="description"
rows="2"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.description}
onChange={handleChange}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Priority
</label>
<select
name="priority"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.priority}
onChange={handleChange}
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Min Detections
</label>
<input
type="number"
name="min_detections"
min="1"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.min_detections}
onChange={handleChange}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Time Window (seconds)
</label>
<input
type="number"
name="time_window"
min="60"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.time_window}
onChange={handleChange}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Cooldown Period (seconds)
</label>
<input
type="number"
name="cooldown_period"
min="0"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.cooldown_period}
onChange={handleChange}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Alert Channels
</label>
<div className="space-y-2">
{['sms', 'email', 'webhook'].map(channel => (
<label key={channel} className="flex items-center">
<input
type="checkbox"
checked={formData.alert_channels.includes(channel)}
onChange={(e) => handleChannelChange(channel, e.target.checked)}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700 capitalize">
{channel}
</span>
</label>
))}
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="submit"
disabled={saving}
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50"
>
{saving ? 'Creating...' : 'Create Rule'}
</button>
<button
type="button"
onClick={onClose}
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
);
};
export default Alerts;

View File

@@ -0,0 +1,325 @@
import React, { useState, useEffect } from 'react';
import { useSocket } from '../contexts/SocketContext';
import api from '../services/api';
import {
ServerIcon,
ExclamationTriangleIcon,
BellIcon,
SignalIcon,
EyeIcon
} from '@heroicons/react/24/outline';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
BarChart,
Bar,
PieChart,
Pie,
Cell
} from 'recharts';
import { format } from 'date-fns';
const Dashboard = () => {
const [overview, setOverview] = useState(null);
const [chartData, setChartData] = useState([]);
const [deviceActivity, setDeviceActivity] = useState([]);
const [recentActivity, setRecentActivity] = useState([]);
const [loading, setLoading] = useState(true);
const { recentDetections, deviceStatus, connected } = useSocket();
useEffect(() => {
fetchDashboardData();
const interval = setInterval(fetchDashboardData, 30000); // Refresh every 30 seconds
return () => clearInterval(interval);
}, []);
const fetchDashboardData = async () => {
try {
const [overviewRes, chartRes, deviceRes, activityRes] = await Promise.all([
api.get('/dashboard/overview?hours=24'),
api.get('/dashboard/charts/detections?hours=24&interval=hour'),
api.get('/dashboard/charts/devices?hours=24'),
api.get('/dashboard/activity?hours=24&limit=10')
]);
setOverview(overviewRes.data.data);
setChartData(chartRes.data.data);
setDeviceActivity(deviceRes.data.data);
setRecentActivity(activityRes.data.data);
} catch (error) {
console.error('Error fetching dashboard data:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary-600"></div>
</div>
);
}
const stats = [
{
id: 1,
name: 'Total Devices',
stat: overview?.summary?.total_devices || 0,
icon: ServerIcon,
change: null,
changeType: 'neutral',
color: 'bg-blue-500'
},
{
id: 2,
name: 'Online Devices',
stat: overview?.summary?.online_devices || 0,
icon: SignalIcon,
change: null,
changeType: 'positive',
color: 'bg-green-500'
},
{
id: 3,
name: 'Recent Detections',
stat: overview?.summary?.recent_detections || 0,
icon: ExclamationTriangleIcon,
change: null,
changeType: 'negative',
color: 'bg-red-500'
},
{
id: 4,
name: 'Unique Drones',
stat: overview?.summary?.unique_drones_detected || 0,
icon: EyeIcon,
change: null,
changeType: 'neutral',
color: 'bg-purple-500'
}
];
const deviceStatusData = [
{ name: 'Online', value: overview?.device_status?.online || 0, color: '#22c55e' },
{ name: 'Offline', value: overview?.device_status?.offline || 0, color: '#ef4444' },
{ name: 'Inactive', value: overview?.device_status?.inactive || 0, color: '#6b7280' }
];
return (
<div className="space-y-6">
{/* Stats */}
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">
System Overview
</h3>
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((item) => (
<div
key={item.id}
className="relative bg-white pt-5 px-4 pb-12 sm:pt-6 sm:px-6 shadow rounded-lg overflow-hidden"
>
<dt>
<div className={`absolute ${item.color} rounded-md p-3`}>
<item.icon className="h-6 w-6 text-white" aria-hidden="true" />
</div>
<p className="ml-16 text-sm font-medium text-gray-500 truncate">
{item.name}
</p>
</dt>
<dd className="ml-16 pb-6 flex items-baseline sm:pb-7">
<p className="text-2xl font-semibold text-gray-900">
{item.stat}
</p>
</dd>
</div>
))}
</dl>
</div>
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Detection Timeline */}
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-medium text-gray-900 mb-4">
Detections Timeline (24h)
</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="timestamp"
tickFormatter={(value) => format(new Date(value), 'HH:mm')}
/>
<YAxis />
<Tooltip
labelFormatter={(value) => format(new Date(value), 'MMM dd, HH:mm')}
/>
<Area
type="monotone"
dataKey="count"
stroke="#ef4444"
fill="#ef4444"
fillOpacity={0.3}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
{/* Device Status */}
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-medium text-gray-900 mb-4">
Device Status
</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={deviceStatusData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={5}
dataKey="value"
>
{deviceStatusData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</div>
<div className="mt-4 flex justify-center space-x-4">
{deviceStatusData.map((item, index) => (
<div key={index} className="flex items-center">
<div
className="w-3 h-3 rounded-full mr-2"
style={{ backgroundColor: item.color }}
/>
<span className="text-sm text-gray-600">
{item.name}: {item.value}
</span>
</div>
))}
</div>
</div>
</div>
{/* Device Activity */}
{deviceActivity.length > 0 && (
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-medium text-gray-900 mb-4">
Device Activity (24h)
</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={deviceActivity}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="device_name"
angle={-45}
textAnchor="end"
height={60}
/>
<YAxis />
<Tooltip />
<Bar dataKey="detection_count" fill="#3b82f6" />
</BarChart>
</ResponsiveContainer>
</div>
</div>
)}
{/* Recent Activity & Real-time Detections */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Recent Activity */}
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">Recent Activity</h3>
</div>
<div className="divide-y divide-gray-200 max-h-96 overflow-y-auto">
{recentActivity.map((activity, index) => (
<div key={index} className="px-6 py-4">
<div className="flex items-center space-x-3">
<div className={`flex-shrink-0 w-2 h-2 rounded-full ${
activity.type === 'detection' ? 'bg-red-400' : 'bg-green-400'
}`} />
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-900">
{activity.type === 'detection' ? (
<>Drone {activity.data.drone_id} detected by {activity.data.device_name}</>
) : (
<>Heartbeat from {activity.data.device_name}</>
)}
</p>
<p className="text-xs text-gray-500">
{format(new Date(activity.timestamp), 'MMM dd, HH:mm:ss')}
</p>
</div>
</div>
</div>
))}
{recentActivity.length === 0 && (
<div className="px-6 py-8 text-center text-gray-500">
No recent activity
</div>
)}
</div>
</div>
{/* Real-time Detections */}
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h3 className="text-lg font-medium text-gray-900">Live Detections</h3>
<div className={`flex items-center space-x-2 px-2 py-1 rounded-full text-xs ${
connected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
<div className={`w-2 h-2 rounded-full ${
connected ? 'bg-green-400' : 'bg-red-400'
}`} />
<span>{connected ? 'Live' : 'Disconnected'}</span>
</div>
</div>
<div className="divide-y divide-gray-200 max-h-96 overflow-y-auto">
{recentDetections.map((detection, index) => (
<div key={index} className="px-6 py-4 animate-fade-in">
<div className="flex items-center space-x-3">
<div className="flex-shrink-0 w-2 h-2 rounded-full bg-red-400 animate-pulse" />
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-900">
Drone {detection.drone_id} detected
</p>
<p className="text-xs text-gray-500">
{detection.device.name || `Device ${detection.device_id}`}
RSSI: {detection.rssi}dBm
Freq: {detection.freq}MHz
</p>
<p className="text-xs text-gray-500">
{format(new Date(detection.server_timestamp), 'HH:mm:ss')}
</p>
</div>
</div>
</div>
))}
{recentDetections.length === 0 && (
<div className="px-6 py-8 text-center text-gray-500">
No recent detections
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default Dashboard;

View File

@@ -0,0 +1,323 @@
import React, { useState, useEffect } from 'react';
import api from '../services/api';
import { format } from 'date-fns';
import {
MagnifyingGlassIcon,
FunnelIcon,
EyeIcon
} from '@heroicons/react/24/outline';
const Detections = () => {
const [detections, setDetections] = useState([]);
const [loading, setLoading] = useState(true);
const [pagination, setPagination] = useState({});
const [filters, setFilters] = useState({
device_id: '',
drone_id: '',
start_date: '',
end_date: '',
limit: 50,
offset: 0
});
const [showFilters, setShowFilters] = useState(false);
useEffect(() => {
fetchDetections();
}, [filters]);
const fetchDetections = async () => {
try {
setLoading(true);
const params = new URLSearchParams();
Object.entries(filters).forEach(([key, value]) => {
if (value) params.append(key, value);
});
const response = await api.get(`/detections?${params}`);
setDetections(response.data.data);
setPagination(response.data.pagination);
} catch (error) {
console.error('Error fetching detections:', error);
} finally {
setLoading(false);
}
};
const handleFilterChange = (key, value) => {
setFilters(prev => ({
...prev,
[key]: value,
offset: 0 // Reset to first page when filtering
}));
};
const handlePageChange = (newOffset) => {
setFilters(prev => ({
...prev,
offset: newOffset
}));
};
const clearFilters = () => {
setFilters({
device_id: '',
drone_id: '',
start_date: '',
end_date: '',
limit: 50,
offset: 0
});
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">
Drone Detections
</h3>
<p className="mt-1 text-sm text-gray-500">
History of all drone detections from your devices
</p>
</div>
<button
onClick={() => setShowFilters(!showFilters)}
className="btn btn-secondary flex items-center space-x-2"
>
<FunnelIcon className="h-4 w-4" />
<span>Filters</span>
</button>
</div>
{/* Filters */}
{showFilters && (
<div className="bg-white p-6 rounded-lg shadow border">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Device ID
</label>
<input
type="number"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Device ID"
value={filters.device_id}
onChange={(e) => handleFilterChange('device_id', e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Drone ID
</label>
<input
type="number"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Drone ID"
value={filters.drone_id}
onChange={(e) => handleFilterChange('drone_id', e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Start Date
</label>
<input
type="datetime-local"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={filters.start_date}
onChange={(e) => handleFilterChange('start_date', e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
End Date
</label>
<input
type="datetime-local"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={filters.end_date}
onChange={(e) => handleFilterChange('end_date', e.target.value)}
/>
</div>
</div>
<div className="mt-4 flex space-x-2">
<button
onClick={clearFilters}
className="btn btn-secondary"
>
Clear Filters
</button>
</div>
</div>
)}
{/* Detection List */}
<div className="bg-white rounded-lg shadow overflow-hidden">
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
) : (
<>
<div className="table-responsive">
<table className="table">
<thead>
<tr>
<th>Device</th>
<th>Drone ID</th>
<th>Type</th>
<th>Frequency</th>
<th>RSSI</th>
<th>Location</th>
<th>Detected At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{detections.map((detection) => (
<tr key={detection.id} className="hover:bg-gray-50">
<td>
<div>
<div className="text-sm font-medium text-gray-900">
{detection.device?.name || `Device ${detection.device_id}`}
</div>
<div className="text-sm text-gray-500">
ID: {detection.device_id}
</div>
</div>
</td>
<td>
<span className="px-2 py-1 bg-red-100 text-red-800 rounded-full text-sm font-medium">
{detection.drone_id}
</span>
</td>
<td>
<span className="text-sm text-gray-900">
{detection.drone_type === 0 ? 'Unknown' : `Type ${detection.drone_type}`}
</span>
</td>
<td>
<span className="text-sm text-gray-900">
{detection.freq} MHz
</span>
</td>
<td>
<span className={`text-sm font-medium ${
detection.rssi > -60 ? 'text-red-600' :
detection.rssi > -80 ? 'text-yellow-600' : 'text-green-600'
}`}>
{detection.rssi} dBm
</span>
</td>
<td>
<div className="text-sm text-gray-900">
{detection.device?.location_description ||
(detection.geo_lat && detection.geo_lon ?
`${detection.geo_lat}, ${detection.geo_lon}` :
'Unknown')}
</div>
</td>
<td>
<div className="text-sm text-gray-900">
{format(new Date(detection.server_timestamp), 'MMM dd, yyyy')}
</div>
<div className="text-sm text-gray-500">
{format(new Date(detection.server_timestamp), 'HH:mm:ss')}
</div>
</td>
<td>
<button
className="text-primary-600 hover:text-primary-900 text-sm"
onClick={() => {
// TODO: Open detection details modal
console.log('View detection details:', detection);
}}
>
<EyeIcon className="h-4 w-4" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{detections.length === 0 && !loading && (
<div className="text-center py-12">
<MagnifyingGlassIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No detections found</h3>
<p className="mt-1 text-sm text-gray-500">
Try adjusting your search filters.
</p>
</div>
)}
{/* Pagination */}
{pagination.total > 0 && (
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => handlePageChange(Math.max(0, filters.offset - filters.limit))}
disabled={filters.offset === 0}
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
Previous
</button>
<button
onClick={() => handlePageChange(filters.offset + filters.limit)}
disabled={filters.offset + filters.limit >= pagination.total}
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
Next
</button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing{' '}
<span className="font-medium">{filters.offset + 1}</span>
{' '}to{' '}
<span className="font-medium">
{Math.min(filters.offset + filters.limit, pagination.total)}
</span>
{' '}of{' '}
<span className="font-medium">{pagination.total}</span>
{' '}results
</p>
</div>
<div>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
<button
onClick={() => handlePageChange(Math.max(0, filters.offset - filters.limit))}
disabled={filters.offset === 0}
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
Previous
</button>
<button
onClick={() => handlePageChange(filters.offset + filters.limit)}
disabled={filters.offset + filters.limit >= pagination.total}
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
Next
</button>
</nav>
</div>
</div>
</div>
)}
</>
)}
</div>
</div>
);
};
export default Detections;

View File

@@ -0,0 +1,437 @@
import React, { useState, useEffect } from 'react';
import api from '../services/api';
import { format } from 'date-fns';
import {
PlusIcon,
PencilIcon,
TrashIcon,
ServerIcon,
MapPinIcon,
SignalIcon,
BoltIcon
} from '@heroicons/react/24/outline';
const Devices = () => {
const [devices, setDevices] = useState([]);
const [loading, setLoading] = useState(true);
const [showAddModal, setShowAddModal] = useState(false);
const [editingDevice, setEditingDevice] = useState(null);
useEffect(() => {
fetchDevices();
}, []);
const fetchDevices = async () => {
try {
const response = await api.get('/devices?include_stats=true');
setDevices(response.data.data);
} catch (error) {
console.error('Error fetching devices:', error);
} finally {
setLoading(false);
}
};
const handleAddDevice = () => {
setEditingDevice(null);
setShowAddModal(true);
};
const handleEditDevice = (device) => {
setEditingDevice(device);
setShowAddModal(true);
};
const handleDeleteDevice = async (deviceId) => {
if (window.confirm('Are you sure you want to deactivate this device?')) {
try {
await api.delete(`/devices/${deviceId}`);
fetchDevices();
} catch (error) {
console.error('Error deleting device:', error);
}
}
};
const getStatusColor = (status) => {
switch (status) {
case 'online':
return 'bg-green-100 text-green-800';
case 'offline':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getSignalStrength = (lastHeartbeat) => {
if (!lastHeartbeat) return 'Unknown';
const timeSince = (new Date() - new Date(lastHeartbeat)) / 1000 / 60; // minutes
if (timeSince < 5) return 'Strong';
if (timeSince < 15) return 'Good';
if (timeSince < 60) return 'Weak';
return 'Lost';
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary-600"></div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">
Devices
</h3>
<p className="mt-1 text-sm text-gray-500">
Manage your drone detection devices
</p>
</div>
<button
onClick={handleAddDevice}
className="btn btn-primary flex items-center space-x-2"
>
<PlusIcon className="h-4 w-4" />
<span>Add Device</span>
</button>
</div>
{/* Device Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{devices.map((device) => (
<div key={device.id} className="bg-white rounded-lg shadow border border-gray-200 hover:shadow-md transition-shadow">
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<div className={`w-3 h-3 rounded-full ${
device.stats?.status === 'online' ? 'bg-green-400' : 'bg-red-400'
}`} />
<h4 className="text-lg font-medium text-gray-900">
{device.name || `Device ${device.id}`}
</h4>
</div>
<div className="flex space-x-1">
<button
onClick={() => handleEditDevice(device)}
className="p-1 text-gray-400 hover:text-gray-600"
>
<PencilIcon className="h-4 w-4" />
</button>
<button
onClick={() => handleDeleteDevice(device.id)}
className="p-1 text-gray-400 hover:text-red-600"
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500">Status</span>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
getStatusColor(device.stats?.status)
}`}>
{device.stats?.status || 'Unknown'}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500">Device ID</span>
<span className="text-sm font-medium text-gray-900">
{device.id}
</span>
</div>
{device.location_description && (
<div className="flex items-start justify-between">
<span className="text-sm text-gray-500">Location</span>
<span className="text-sm text-gray-900 text-right">
{device.location_description}
</span>
</div>
)}
{(device.geo_lat && device.geo_lon) && (
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500">Coordinates</span>
<span className="text-sm text-gray-900">
{device.geo_lat}, {device.geo_lon}
</span>
</div>
)}
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500">Signal</span>
<span className="text-sm text-gray-900">
{getSignalStrength(device.last_heartbeat)}
</span>
</div>
{device.stats && (
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500">Detections (24h)</span>
<span className={`text-sm font-medium ${
device.stats.detections_24h > 0 ? 'text-red-600' : 'text-green-600'
}`}>
{device.stats.detections_24h}
</span>
</div>
)}
{device.last_heartbeat && (
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500">Last Seen</span>
<span className="text-sm text-gray-900">
{format(new Date(device.last_heartbeat), 'MMM dd, HH:mm')}
</span>
</div>
)}
{device.firmware_version && (
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500">Firmware</span>
<span className="text-sm text-gray-900">
{device.firmware_version}
</span>
</div>
)}
</div>
{/* Device Actions */}
<div className="mt-4 pt-4 border-t border-gray-200">
<div className="flex space-x-2">
<button className="flex-1 text-xs bg-gray-100 text-gray-700 py-2 px-3 rounded hover:bg-gray-200 transition-colors">
View Details
</button>
<button className="flex-1 text-xs bg-primary-100 text-primary-700 py-2 px-3 rounded hover:bg-primary-200 transition-colors">
View on Map
</button>
</div>
</div>
</div>
</div>
))}
</div>
{devices.length === 0 && (
<div className="text-center py-12">
<ServerIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No devices</h3>
<p className="mt-1 text-sm text-gray-500">
Get started by adding your first drone detection device.
</p>
<div className="mt-6">
<button
onClick={handleAddDevice}
className="btn btn-primary"
>
<PlusIcon className="h-4 w-4 mr-2" />
Add Device
</button>
</div>
</div>
)}
{/* Add/Edit Device Modal */}
{showAddModal && (
<DeviceModal
device={editingDevice}
onClose={() => setShowAddModal(false)}
onSave={() => {
setShowAddModal(false);
fetchDevices();
}}
/>
)}
</div>
);
};
const DeviceModal = ({ device, onClose, onSave }) => {
const [formData, setFormData] = useState({
id: device?.id || '',
name: device?.name || '',
geo_lat: device?.geo_lat || '',
geo_lon: device?.geo_lon || '',
location_description: device?.location_description || '',
heartbeat_interval: device?.heartbeat_interval || 300,
firmware_version: device?.firmware_version || '',
notes: device?.notes || ''
});
const [saving, setSaving] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setSaving(true);
try {
if (device) {
// Update existing device
await api.put(`/devices/${device.id}`, formData);
} else {
// Create new device
await api.post('/devices', formData);
}
onSave();
} catch (error) {
console.error('Error saving device:', error);
} finally {
setSaving(false);
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onClick={onClose} />
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<form onSubmit={handleSubmit}>
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="mb-4">
<h3 className="text-lg leading-6 font-medium text-gray-900">
{device ? 'Edit Device' : 'Add New Device'}
</h3>
</div>
<div className="space-y-4">
{!device && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Device ID *
</label>
<input
type="number"
name="id"
required
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.id}
onChange={handleChange}
/>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Device Name
</label>
<input
type="text"
name="name"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.name}
onChange={handleChange}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Latitude
</label>
<input
type="number"
step="any"
name="geo_lat"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.geo_lat}
onChange={handleChange}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Longitude
</label>
<input
type="number"
step="any"
name="geo_lon"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.geo_lon}
onChange={handleChange}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Location Description
</label>
<input
type="text"
name="location_description"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.location_description}
onChange={handleChange}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Heartbeat Interval (seconds)
</label>
<input
type="number"
name="heartbeat_interval"
min="60"
max="3600"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.heartbeat_interval}
onChange={handleChange}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Notes
</label>
<textarea
name="notes"
rows="3"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-primary-500 focus:border-primary-500"
value={formData.notes}
onChange={handleChange}
/>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="submit"
disabled={saving}
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50"
>
{saving ? 'Saving...' : (device ? 'Update' : 'Create')}
</button>
<button
type="button"
onClick={onClose}
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
);
};
export default Devices;

134
client/src/pages/Login.jsx Normal file
View File

@@ -0,0 +1,134 @@
import React, { useState } from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
import toast from 'react-hot-toast';
const Login = () => {
const [credentials, setCredentials] = useState({
username: '',
password: ''
});
const [showPassword, setShowPassword] = useState(false);
const { login, loading, isAuthenticated } = useAuth();
if (isAuthenticated) {
return <Navigate to="/" replace />;
}
const handleSubmit = async (e) => {
e.preventDefault();
if (!credentials.username || !credentials.password) {
toast.error('Please fill in all fields');
return;
}
const result = await login(credentials);
if (result.success) {
toast.success('Login successful!');
} else {
toast.error(result.error || 'Login failed');
}
};
const handleChange = (e) => {
setCredentials({
...credentials,
[e.target.name]: e.target.value
});
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<div className="mx-auto h-12 w-12 bg-primary-600 rounded-lg flex items-center justify-center">
<svg className="h-8 w-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Drone Detection System
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Sign in to your account
</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="username" className="sr-only">
Username or Email
</label>
<input
id="username"
name="username"
type="text"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
placeholder="Username or Email"
value={credentials.username}
onChange={handleChange}
disabled={loading}
/>
</div>
<div className="relative">
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type={showPassword ? 'text' : 'password'}
required
className="appearance-none rounded-none relative block w-full px-3 py-2 pr-10 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
placeholder="Password"
value={credentials.password}
onChange={handleChange}
disabled={loading}
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
) : (
<EyeIcon className="h-5 w-5 text-gray-400" />
)}
</button>
</div>
</div>
<div>
<button
type="submit"
disabled={loading}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
) : (
'Sign in'
)}
</button>
</div>
<div className="text-center">
<p className="text-sm text-gray-600">
Demo credentials: <br />
Username: <code className="bg-gray-100 px-1 rounded">admin</code> <br />
Password: <code className="bg-gray-100 px-1 rounded">password</code>
</p>
</div>
</form>
</div>
</div>
);
};
export default Login;

View File

@@ -0,0 +1,283 @@
import React, { useState, useEffect } from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
import { Icon } from 'leaflet';
import { useSocket } from '../contexts/SocketContext';
import api from '../services/api';
import { format } from 'date-fns';
import {
ServerIcon,
ExclamationTriangleIcon,
SignalIcon,
EyeIcon
} from '@heroicons/react/24/outline';
// Fix for default markers in React Leaflet
import 'leaflet/dist/leaflet.css';
import iconRetinaUrl from 'leaflet/dist/images/marker-icon-2x.png';
import iconUrl from 'leaflet/dist/images/marker-icon.png';
import shadowUrl from 'leaflet/dist/images/marker-shadow.png';
delete Icon.Default.prototype._getIconUrl;
Icon.Default.mergeOptions({
iconRetinaUrl,
iconUrl,
shadowUrl,
});
// Custom icons
const createDeviceIcon = (status, hasDetections) => {
let color = '#6b7280'; // gray for offline/inactive
if (status === 'online') {
color = hasDetections ? '#ef4444' : '#22c55e'; // red if detecting, green if online
}
return new Icon({
iconUrl: `data:image/svg+xml;base64,${btoa(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32">
<circle cx="12" cy="12" r="10" fill="${color}" stroke="#fff" stroke-width="2"/>
<path d="M12 8v4l3 3" stroke="#fff" stroke-width="2" fill="none"/>
</svg>
`)}`,
iconSize: [32, 32],
iconAnchor: [16, 16],
popupAnchor: [0, -16],
});
};
const MapView = () => {
const [devices, setDevices] = useState([]);
const [selectedDevice, setSelectedDevice] = useState(null);
const [loading, setLoading] = useState(true);
const [mapCenter, setMapCenter] = useState([59.3293, 18.0686]); // Stockholm default
const [mapZoom, setMapZoom] = useState(10);
const { recentDetections, deviceStatus } = useSocket();
useEffect(() => {
fetchDevices();
const interval = setInterval(fetchDevices, 30000); // Refresh every 30 seconds
return () => clearInterval(interval);
}, []);
const fetchDevices = async () => {
try {
const response = await api.get('/devices/map');
const deviceData = response.data.data;
setDevices(deviceData);
// Set map center to first device with valid coordinates
const deviceWithCoords = deviceData.find(d => d.geo_lat && d.geo_lon);
if (deviceWithCoords && devices.length === 0) {
setMapCenter([deviceWithCoords.geo_lat, deviceWithCoords.geo_lon]);
}
} catch (error) {
console.error('Error fetching devices:', error);
} finally {
setLoading(false);
}
};
const getDeviceStatus = (device) => {
const realtimeStatus = deviceStatus[device.id];
if (realtimeStatus) {
return realtimeStatus.status;
}
return device.status || 'offline';
};
const getDeviceDetections = (deviceId) => {
return recentDetections.filter(d => d.device_id === deviceId);
};
if (loading) {
return (
<div className="flex items-center justify-center h-96">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary-600"></div>
</div>
);
}
return (
<div className="space-y-6">
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">
Device Map
</h3>
<p className="mt-1 text-sm text-gray-500">
Real-time view of all devices and their detection status
</p>
</div>
{/* Map */}
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
<div className="h-96 lg:h-[600px]">
<MapContainer
center={mapCenter}
zoom={mapZoom}
className="h-full w-full"
whenCreated={(map) => {
// Auto-fit to device locations if available
const validDevices = devices.filter(d => d.geo_lat && d.geo_lon);
if (validDevices.length > 1) {
const bounds = validDevices.map(d => [d.geo_lat, d.geo_lon]);
map.fitBounds(bounds, { padding: [20, 20] });
}
}}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{devices
.filter(device => device.geo_lat && device.geo_lon)
.map(device => {
const status = getDeviceStatus(device);
const detections = getDeviceDetections(device.id);
const hasRecentDetections = detections.length > 0;
return (
<Marker
key={device.id}
position={[device.geo_lat, device.geo_lon]}
icon={createDeviceIcon(status, hasRecentDetections)}
eventHandlers={{
click: () => setSelectedDevice(device),
}}
>
<Popup>
<DevicePopup
device={device}
status={status}
detections={detections}
/>
</Popup>
</Marker>
);
})}
</MapContainer>
</div>
</div>
{/* Device List */}
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">Device Status</h3>
</div>
<div className="divide-y divide-gray-200">
{devices.map(device => {
const status = getDeviceStatus(device);
const detections = getDeviceDetections(device.id);
return (
<DeviceListItem
key={device.id}
device={device}
status={status}
detections={detections}
onClick={() => setSelectedDevice(device)}
/>
);
})}
{devices.length === 0 && (
<div className="px-6 py-8 text-center text-gray-500">
No devices found
</div>
)}
</div>
</div>
</div>
);
};
const DevicePopup = ({ device, status, detections }) => (
<div className="p-2 min-w-[200px]">
<div className="flex items-center justify-between mb-2">
<h4 className="font-semibold text-gray-900">
{device.name || `Device ${device.id}`}
</h4>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
status === 'online'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{status}
</span>
</div>
{device.location_description && (
<p className="text-sm text-gray-600 mb-2">
{device.location_description}
</p>
)}
<div className="text-xs text-gray-500 space-y-1">
<div>ID: {device.id}</div>
<div>Coordinates: {device.geo_lat}, {device.geo_lon}</div>
{device.last_heartbeat && (
<div>
Last seen: {format(new Date(device.last_heartbeat), 'MMM dd, HH:mm')}
</div>
)}
</div>
{detections.length > 0 && (
<div className="mt-3 pt-2 border-t border-gray-200">
<div className="flex items-center space-x-1 text-red-600 text-sm font-medium mb-1">
<ExclamationTriangleIcon className="h-4 w-4" />
<span>{detections.length} recent detection{detections.length > 1 ? 's' : ''}</span>
</div>
{detections.slice(0, 3).map((detection, index) => (
<div key={index} className="text-xs text-gray-600">
Drone {detection.drone_id} {detection.freq}MHz {detection.rssi}dBm
</div>
))}
</div>
)}
</div>
);
const DeviceListItem = ({ device, status, detections, onClick }) => (
<div
className="px-6 py-4 hover:bg-gray-50 cursor-pointer transition-colors"
onClick={onClick}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className={`w-3 h-3 rounded-full ${
status === 'online'
? detections.length > 0 ? 'bg-red-400 animate-pulse' : 'bg-green-400'
: 'bg-gray-400'
}`} />
<div>
<div className="text-sm font-medium text-gray-900">
{device.name || `Device ${device.id}`}
</div>
<div className="text-sm text-gray-500">
{device.location_description || `${device.geo_lat}, ${device.geo_lon}`}
</div>
</div>
</div>
<div className="flex items-center space-x-4">
{detections.length > 0 && (
<div className="flex items-center space-x-1 text-red-600">
<ExclamationTriangleIcon className="h-4 w-4" />
<span className="text-sm font-medium">{detections.length}</span>
</div>
)}
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
status === 'online'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{status}
</span>
</div>
</div>
</div>
);
export default MapView;

View File

@@ -0,0 +1,41 @@
import axios from 'axios';
const API_BASE_URL = process.env.NODE_ENV === 'production'
? '/api'
: 'http://localhost:3001/api';
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor to add auth token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor for error handling
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Token expired or invalid
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;

77
client/tailwind.config.js Normal file
View File

@@ -0,0 +1,77 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
},
danger: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
},
success: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
},
warning: {
50: '#fffbeb',
100: '#fef3c7',
200: '#fde68a',
300: '#fcd34d',
400: '#fbbf24',
500: '#f59e0b',
600: '#d97706',
700: '#b45309',
800: '#92400e',
900: '#78350f',
}
},
animation: {
'fade-in': 'fadeIn 0.5s ease-in-out',
'slide-up': 'slideUp 0.3s ease-out',
'pulse-slow': 'pulse 3s infinite',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
}
},
},
plugins: [],
}

19
client/vite.config.js Normal file
View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true
}
}
},
build: {
outDir: 'dist',
sourcemap: true
}
})

152
docker-compose.yml Normal file
View File

@@ -0,0 +1,152 @@
version: '3.8'
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: drone-detection-db
restart: unless-stopped
environment:
POSTGRES_DB: drone_detection
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres123
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- postgres_data:/var/lib/postgresql/data
- ./server/scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
ports:
- "5432:5432"
networks:
- drone-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d drone_detection"]
interval: 30s
timeout: 10s
retries: 3
# Redis for session management and caching
redis:
image: redis:7-alpine
container_name: drone-detection-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis_data:/data
ports:
- "6379:6379"
networks:
- drone-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
# Backend API Server
backend:
build:
context: ./server
dockerfile: Dockerfile
container_name: drone-detection-backend
restart: unless-stopped
environment:
NODE_ENV: production
PORT: 3001
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: drone_detection
DB_USER: postgres
DB_PASSWORD: postgres123
REDIS_HOST: redis
REDIS_PORT: 6379
JWT_SECRET: ${JWT_SECRET:-your-super-secret-jwt-key-change-in-production}
TWILIO_ACCOUNT_SID: ${TWILIO_ACCOUNT_SID}
TWILIO_AUTH_TOKEN: ${TWILIO_AUTH_TOKEN}
TWILIO_PHONE_NUMBER: ${TWILIO_PHONE_NUMBER}
CORS_ORIGIN: http://localhost:3000
ports:
- "3001:3001"
volumes:
- ./server/logs:/app/logs
networks:
- drone-network
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/api/health"]
interval: 30s
timeout: 10s
retries: 3
# Frontend React Application
frontend:
build:
context: ./client
dockerfile: Dockerfile
args:
VITE_API_URL: http://localhost:3001/api
VITE_WS_URL: ws://localhost:3001
container_name: drone-detection-frontend
restart: unless-stopped
ports:
- "3000:80"
networks:
- drone-network
depends_on:
- backend
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80"]
interval: 30s
timeout: 10s
retries: 3
# Nginx Reverse Proxy (Production)
nginx:
image: nginx:alpine
container_name: drone-detection-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
- ./docker/ssl:/etc/nginx/ssl
- ./client/dist:/usr/share/nginx/html
networks:
- drone-network
depends_on:
- frontend
- backend
profiles:
- production
# Python Drone Simulator (Optional)
simulator:
build:
context: .
dockerfile: docker/simulator/Dockerfile
container_name: drone-detection-simulator
restart: "no"
environment:
API_URL: http://backend:3001/api
networks:
- drone-network
depends_on:
- backend
profiles:
- simulation
command: python drone_simulator.py --devices 5 --duration 3600
volumes:
postgres_data:
driver: local
redis_data:
driver: local
networks:
drone-network:
driver: bridge

154
docker-start.bat Normal file
View File

@@ -0,0 +1,154 @@
@echo off
setlocal enabledelayedexpansion
REM Drone Detection System - Docker Quick Start Script (Windows)
REM This script sets up and starts the complete system using Docker
echo 🐳 Drone Detection System - Docker Setup
echo ========================================
REM Check if Docker is installed
docker --version >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Docker is not installed. Please install Docker Desktop first.
pause
exit /b 1
)
REM Check if Docker Compose is installed
docker-compose --version >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Docker Compose is not installed. Please install Docker Compose first.
pause
exit /b 1
)
echo [INFO] Docker and Docker Compose are available
REM Create .env file if it doesn't exist
if not exist .env (
echo [INFO] Creating .env file from template...
copy .env.docker .env >nul
echo [WARNING] Please edit .env file with your Twilio credentials before continuing
echo Required variables:
echo - TWILIO_ACCOUNT_SID
echo - TWILIO_AUTH_TOKEN
echo - TWILIO_PHONE_NUMBER
echo.
pause
)
REM Parse command line arguments
set PROFILE=default
set DETACH=-d
set BUILD=
:parse_args
if "%~1"=="" goto start_services
if "%~1"=="--production" set PROFILE=production
if "%~1"=="-p" set PROFILE=production
if "%~1"=="--simulation" set PROFILE=simulation
if "%~1"=="-s" set PROFILE=simulation
if "%~1"=="--foreground" set DETACH=
if "%~1"=="-f" set DETACH=
if "%~1"=="--build" set BUILD=--build
if "%~1"=="-b" set BUILD=--build
if "%~1"=="--help" goto show_help
if "%~1"=="-h" goto show_help
shift
goto parse_args
:show_help
echo Usage: %~nx0 [OPTIONS]
echo.
echo Options:
echo -p, --production Run with production profile (includes Nginx)
echo -s, --simulation Run with simulation profile (includes drone simulator)
echo -f, --foreground Run in foreground (don't detach)
echo -b, --build Force rebuild of containers
echo -h, --help Show this help message
echo.
echo Examples:
echo %~nx0 # Start basic system
echo %~nx0 -p # Start with production setup
echo %~nx0 -s -f # Start with simulation in foreground
echo %~nx0 -b # Rebuild and start
pause
exit /b 0
:start_services
REM Stop any existing containers
echo [INFO] Stopping any existing containers...
docker-compose down 2>nul
REM Build containers if requested
if not "%BUILD%"=="" (
echo [INFO] Building Docker containers...
docker-compose build
)
REM Start the appropriate profile
if "%PROFILE%"=="production" (
echo [INFO] Starting production environment...
docker-compose --profile production up %DETACH% %BUILD%
) else if "%PROFILE%"=="simulation" (
echo [INFO] Starting with simulation environment...
docker-compose --profile simulation up %DETACH% %BUILD%
) else (
echo [INFO] Starting development environment...
docker-compose up %DETACH% %BUILD%
)
REM Wait a moment for services to start
if not "%DETACH%"=="" (
echo [INFO] Waiting for services to start...
timeout /t 10 /nobreak >nul
echo [INFO] Checking service health...
REM Check backend
curl -sf http://localhost:3001/api/health >nul 2>&1
if %errorlevel% equ 0 (
echo [SUCCESS] Backend is healthy
) else (
echo [WARNING] Backend health check failed
)
REM Check frontend
curl -sf http://localhost:3000 >nul 2>&1
if %errorlevel% equ 0 (
echo [SUCCESS] Frontend is healthy
) else (
echo [WARNING] Frontend health check failed
)
echo.
echo [SUCCESS] 🎉 Drone Detection System is running!
echo.
echo Access URLs:
echo Frontend: http://localhost:3000
echo Backend: http://localhost:3001/api
echo Health: http://localhost:3001/api/health
echo.
echo Default login credentials:
echo Admin: admin / admin123
echo Operator: operator / operator123
echo.
echo Useful commands:
echo docker-compose logs -f # View logs
echo docker-compose ps # Check status
echo docker-compose down # Stop services
echo docker-compose restart backend # Restart a service
echo.
if "%PROFILE%"=="simulation" (
echo 🐍 Simulation is running!
echo Monitor with: docker-compose logs -f simulator
echo.
)
) else (
echo.
echo [INFO] Services are running in foreground. Press Ctrl+C to stop.
)
pause

191
docker-start.sh Normal file
View File

@@ -0,0 +1,191 @@
#!/bin/bash
# Drone Detection System - Docker Quick Start Script
# This script sets up and starts the complete system using Docker
set -e
echo "🐳 Drone Detection System - Docker Setup"
echo "========================================"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if Docker is installed
if ! command -v docker &> /dev/null; then
print_error "Docker is not installed. Please install Docker first."
exit 1
fi
# Check if Docker Compose is installed
if ! command -v docker-compose &> /dev/null; then
print_error "Docker Compose is not installed. Please install Docker Compose first."
exit 1
fi
print_status "Docker and Docker Compose are available"
# Create .env file if it doesn't exist
if [ ! -f .env ]; then
print_status "Creating .env file from template..."
cp .env.docker .env
print_warning "Please edit .env file with your Twilio credentials before continuing"
echo "Required variables:"
echo " - TWILIO_ACCOUNT_SID"
echo " - TWILIO_AUTH_TOKEN"
echo " - TWILIO_PHONE_NUMBER"
echo ""
read -p "Press Enter to continue when .env is configured..."
fi
# Parse command line arguments
PROFILE="default"
DETACH="-d"
BUILD=""
while [[ $# -gt 0 ]]; do
case $1 in
--production|-p)
PROFILE="production"
shift
;;
--simulation|-s)
PROFILE="simulation"
shift
;;
--foreground|-f)
DETACH=""
shift
;;
--build|-b)
BUILD="--build"
shift
;;
--help|-h)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " -p, --production Run with production profile (includes Nginx)"
echo " -s, --simulation Run with simulation profile (includes drone simulator)"
echo " -f, --foreground Run in foreground (don't detach)"
echo " -b, --build Force rebuild of containers"
echo " -h, --help Show this help message"
echo ""
echo "Examples:"
echo " $0 # Start basic system"
echo " $0 -p # Start with production setup"
echo " $0 -s -f # Start with simulation in foreground"
echo " $0 -b # Rebuild and start"
exit 0
;;
*)
print_error "Unknown option: $1"
exit 1
;;
esac
done
# Stop any existing containers
print_status "Stopping any existing containers..."
docker-compose down 2>/dev/null || true
# Build containers if requested
if [ -n "$BUILD" ]; then
print_status "Building Docker containers..."
docker-compose build
fi
# Start the appropriate profile
case $PROFILE in
"production")
print_status "Starting production environment..."
docker-compose --profile production up $DETACH $BUILD
;;
"simulation")
print_status "Starting with simulation environment..."
docker-compose --profile simulation up $DETACH $BUILD
;;
*)
print_status "Starting development environment..."
docker-compose up $DETACH $BUILD
;;
esac
# Wait a moment for services to start
if [ -n "$DETACH" ]; then
print_status "Waiting for services to start..."
sleep 10
# Check service health
print_status "Checking service health..."
# Check backend
if curl -sf http://localhost:3001/api/health > /dev/null 2>&1; then
print_success "Backend is healthy"
else
print_warning "Backend health check failed"
fi
# Check frontend
if curl -sf http://localhost:3000 > /dev/null 2>&1; then
print_success "Frontend is healthy"
else
print_warning "Frontend health check failed"
fi
# Check database
if docker-compose exec -T postgres pg_isready -U postgres > /dev/null 2>&1; then
print_success "Database is healthy"
else
print_warning "Database health check failed"
fi
echo ""
print_success "🎉 Drone Detection System is running!"
echo ""
echo "Access URLs:"
echo " Frontend: http://localhost:3000"
echo " Backend: http://localhost:3001/api"
echo " Health: http://localhost:3001/api/health"
echo ""
echo "Default login credentials:"
echo " Admin: admin / admin123"
echo " Operator: operator / operator123"
echo ""
echo "Useful commands:"
echo " docker-compose logs -f # View logs"
echo " docker-compose ps # Check status"
echo " docker-compose down # Stop services"
echo " docker-compose restart backend # Restart a service"
echo ""
if [ "$PROFILE" = "simulation" ]; then
echo "🐍 Simulation is running!"
echo " Monitor with: docker-compose logs -f simulator"
echo ""
fi
else
echo ""
print_status "Services are running in foreground. Press Ctrl+C to stop."
fi

85
docker/nginx/default.conf Normal file
View File

@@ -0,0 +1,85 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: ws: wss: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private must-revalidate auth;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/xml+rss
application/json;
# Handle React Router (SPA)
location / {
try_files $uri $uri/ /index.html;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# Proxy API requests to backend
location /api {
proxy_pass http://backend:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 86400;
}
# WebSocket proxy for Socket.IO
location /socket.io/ {
proxy_pass http://backend:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# Security.txt
location /.well-known/security.txt {
return 200 "Contact: security@example.com\nExpires: 2025-12-31T23:59:59.000Z\n";
add_header Content-Type text/plain;
}
# Deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}

61
docker/nginx/nginx.conf Normal file
View File

@@ -0,0 +1,61 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging format
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
# Performance optimizations
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 16M;
# Hide nginx version
server_tokens off;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_proxied any;
gzip_types
text/plain
text/css
text/xml
text/javascript
text/x-component
application/javascript
application/x-javascript
application/xml
application/json
application/xhtml+xml
application/rss+xml
application/atom+xml
image/svg+xml;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
# Include server configurations
include /etc/nginx/conf.d/*.conf;
}

View File

@@ -0,0 +1,32 @@
# Python Simulator Dockerfile for Drone Detection System
FROM python:3.11-alpine AS base
# Install system dependencies
RUN apk add --no-cache \
curl \
&& rm -rf /var/cache/apk/*
# Set working directory
WORKDIR /app
# Copy requirements file
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy simulator script
COPY drone_simulator.py .
# Create non-root user
RUN addgroup -g 1001 -S simulator && \
adduser -S simulator -u 1001
# Set ownership
RUN chown -R simulator:simulator /app
# Switch to non-root user
USER simulator
# Default command (can be overridden)
CMD ["python", "drone_simulator.py", "--api-url", "http://backend:3001/api", "--devices", "5", "--duration", "3600"]

393
docs/DOCKER_DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,393 @@
# Docker Deployment Guide
This guide covers deploying the Drone Detection System using Docker and Docker Compose.
## Prerequisites
- Docker Engine 20.10+
- Docker Compose 2.0+
- At least 4GB RAM
- 10GB available disk space
## Quick Start
### 1. Environment Setup
```bash
# Copy environment template
cp .env.docker .env
# Edit .env with your Twilio credentials
nano .env
```
### 2. Basic Deployment
```bash
# Build and start all services
docker-compose up -d
# View logs
docker-compose logs -f
# Check service status
docker-compose ps
```
### 3. Access the Application
- **Frontend**: http://localhost:3000
- **Backend API**: http://localhost:3001/api
- **Database**: localhost:5432
- **Redis**: localhost:6379
## Service Architecture
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Frontend │ │ Backend │ │ PostgreSQL │
│ (React) │◄──►│ (Node.js) │◄──►│ Database │
│ Port: 3000 │ │ Port: 3001 │ │ Port: 5432 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ ┌─────────────────┐ │
└──────────────►│ Redis │◄────────────┘
│ (Caching) │
│ Port: 6379 │
└─────────────────┘
```
## Docker Compose Profiles
### Development Profile (Default)
```bash
docker-compose up -d
```
Includes: Frontend, Backend, Database, Redis
### Production Profile
```bash
docker-compose --profile production up -d
```
Includes: All services + Nginx reverse proxy
### Simulation Profile
```bash
docker-compose --profile simulation up -d
```
Includes: All services + Python drone simulator
## Service Details
### Frontend Container
- **Image**: Custom Nginx + React build
- **Port**: 3000:80
- **Features**:
- Gzip compression
- SPA routing support
- API proxying
- Security headers
### Backend Container
- **Image**: Node.js 18 Alpine
- **Port**: 3001:3001
- **Features**:
- Health checks
- Non-root user
- Log persistence
- Signal handling
### Database Container
- **Image**: PostgreSQL 15 Alpine
- **Port**: 5432:5432
- **Features**:
- Persistent storage
- Health checks
- Initialization scripts
- Performance tuning
### Redis Container
- **Image**: Redis 7 Alpine
- **Port**: 6379:6379
- **Features**:
- Persistent storage
- AOF logging
- Health checks
### Nginx Proxy (Production)
- **Image**: Nginx Alpine
- **Ports**: 80:80, 443:443
- **Features**:
- SSL termination
- Load balancing
- Static file serving
- WebSocket support
## Environment Variables
### Backend Environment
```bash
NODE_ENV=production
PORT=3001
DB_HOST=postgres
DB_PORT=5432
DB_NAME=drone_detection
DB_USER=postgres
DB_PASSWORD=postgres123
REDIS_HOST=redis
REDIS_PORT=6379
JWT_SECRET=your-jwt-secret
TWILIO_ACCOUNT_SID=your-twilio-sid
TWILIO_AUTH_TOKEN=your-twilio-token
TWILIO_PHONE_NUMBER=your-twilio-phone
CORS_ORIGIN=http://localhost:3000
```
### Frontend Build Arguments
```bash
VITE_API_URL=http://localhost:3001/api
VITE_WS_URL=ws://localhost:3001
```
## Data Persistence
### Volumes
- `postgres_data`: Database files
- `redis_data`: Redis persistence
- `./server/logs`: Application logs
### Backup Strategy
```bash
# Database backup
docker-compose exec postgres pg_dump -U postgres drone_detection > backup.sql
# Restore database
docker-compose exec -T postgres psql -U postgres drone_detection < backup.sql
# Volume backup
docker run --rm -v uamils_postgres_data:/data -v $(pwd):/backup alpine tar czf /backup/postgres_backup.tar.gz /data
```
## Monitoring and Logs
### View Logs
```bash
# All services
docker-compose logs -f
# Specific service
docker-compose logs -f backend
# Last 100 lines
docker-compose logs --tail=100 backend
```
### Health Checks
```bash
# Check service health
docker-compose ps
# Manual health check
curl http://localhost:3001/api/health
curl http://localhost:3000/health
```
### Resource Monitoring
```bash
# Container stats
docker stats
# Detailed container info
docker-compose exec backend top
```
## Troubleshooting
### Common Issues
#### 1. Database Connection Issues
```bash
# Check database status
docker-compose exec postgres pg_isready -U postgres
# View database logs
docker-compose logs postgres
# Reset database
docker-compose down -v
docker-compose up -d
```
#### 2. Frontend Build Issues
```bash
# Rebuild frontend
docker-compose build --no-cache frontend
# Check build logs
docker-compose logs frontend
```
#### 3. Backend API Issues
```bash
# Check backend health
curl http://localhost:3001/api/health/detailed
# View backend logs
docker-compose logs backend
# Restart backend
docker-compose restart backend
```
#### 4. Port Conflicts
```bash
# Check port usage
netstat -tulpn | grep :3000
netstat -tulpn | grep :3001
# Stop conflicting services
docker-compose down
```
### Performance Tuning
#### 1. Database Optimization
```bash
# Increase shared_buffers for PostgreSQL
docker-compose exec postgres psql -U postgres -c "ALTER SYSTEM SET shared_buffers = '256MB';"
docker-compose restart postgres
```
#### 2. Memory Limits
```yaml
# Add to docker-compose.yml services
services:
backend:
mem_limit: 512m
mem_reservation: 256m
frontend:
mem_limit: 256m
mem_reservation: 128m
```
## Production Deployment
### 1. SSL Configuration
```bash
# Generate SSL certificates
mkdir -p docker/ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout docker/ssl/nginx.key \
-out docker/ssl/nginx.crt
```
### 2. Environment Security
```bash
# Use Docker secrets for sensitive data
echo "your-jwt-secret" | docker secret create jwt_secret -
echo "your-twilio-token" | docker secret create twilio_token -
```
### 3. Nginx Configuration
```bash
# Enable production profile
docker-compose --profile production up -d
# Update nginx config for your domain
# Edit docker/nginx/default.conf
```
### 4. Monitoring Setup
```bash
# Add monitoring services
docker-compose -f docker-compose.yml -f docker-compose.monitoring.yml up -d
```
## Scaling
### Horizontal Scaling
```yaml
# Scale backend instances
docker-compose up -d --scale backend=3
# Load balancer configuration required
```
### Database Scaling
```yaml
# Add read replicas
postgres-replica:
image: postgres:15-alpine
environment:
POSTGRES_MASTER_SERVICE: postgres
POSTGRES_REPLICA_USER: replica
POSTGRES_REPLICA_PASSWORD: replica123
```
## Maintenance
### Updates
```bash
# Update images
docker-compose pull
# Rebuild and restart
docker-compose down
docker-compose up -d --build
```
### Cleanup
```bash
# Remove unused containers
docker system prune
# Remove unused volumes
docker volume prune
# Clean build cache
docker builder prune
```
## Testing with Simulator
### Run Simulation
```bash
# Start simulation profile
docker-compose --profile simulation up -d
# Run custom simulation
docker-compose run --rm simulator python drone_simulator.py \
--devices 10 \
--duration 3600 \
--detection-interval 30
```
### Monitor Simulation
```bash
# View simulator logs
docker-compose logs -f simulator
# Check API stats
curl http://localhost:3001/api/dashboard/stats
```
## Security Considerations
### Container Security
- Non-root users in all containers
- Read-only root filesystems where possible
- Limited container capabilities
- Security scanning with `docker scan`
### Network Security
- Custom bridge network isolation
- No unnecessary port exposures
- Internal service communication
### Data Security
- Encrypted environment variables
- SSL/TLS termination at proxy
- Database connection encryption
- Regular security updates
For additional security hardening, see [Security Best Practices](../docs/SECURITY.md).

View File

@@ -0,0 +1,141 @@
# Enhanced Drone Detection System - Threat Assessment Summary
## 🚨 Security Enhancements for Government Sites
Your drone detection system has been significantly enhanced with intelligent threat assessment capabilities specifically designed for Swedish government sites, water facilities, nuclear plants, and other sensitive installations.
## 🎯 Key Security Features Added
### 1. **RSSI-Based Threat Classification**
- **Critical Threats** (0-50m): Immediate security response
- **High Threats** (50-200m): Security response recommended
- **Medium Threats** (200m-1km): Enhanced monitoring
- **Low Threats** (1-5km): Standard monitoring
- **Monitoring** (5-15km): Passive surveillance
### 2. **Intelligent Distance Calculation**
- Real-time distance estimation using RSSI signal strength
- Path loss calculations adapted for outdoor security environments
- Accurate threat zone determination for perimeter security
### 3. **Enhanced Alert System**
- **Critical threats automatically trigger all alert channels**
- Threat-specific alert messages with security descriptions
- Immediate action notifications for high-priority threats
- Bypasses cooldown periods for critical security situations
### 4. **Swedish Location Integration**
Pre-configured monitoring for sensitive Swedish facilities:
- Government offices and Riksdag
- Water treatment facilities (Norsborg, Lovö, etc.)
- Nuclear power plants (Forsmark, Ringhals, Oskarshamn)
- Military installations (Karlsborg, Boden, etc.)
- Major airports (Arlanda, Landvetter, etc.)
## 🐍 Python Simulation Script
### Comprehensive Testing Tool
The `drone_simulator.py` script provides realistic testing with:
- **Swedish coordinates** for actual sensitive locations
- **Threat-based scenarios** with realistic probability distributions
- **RSSI calculations** based on actual physics formulas
- **Continuous device monitoring** with heartbeat simulation
- **Multiple facility types** (government, water, nuclear, military)
### Usage Examples
```bash
# Basic simulation with 5 devices
python drone_simulator.py
# Extended simulation for stress testing
python drone_simulator.py --devices 15 --duration 7200 --detection-interval 30
# List all available Swedish monitoring locations
python drone_simulator.py --list-locations
```
## 📊 Threat Statistics
The simulator generates realistic threat distributions:
- **70%** - Low threats (5-15km range)
- **20%** - Medium threats (200m-5km range)
- **8%** - High threats (50-200m range)
- **2%** - Critical threats (0-50m range)
## 🔧 Implementation Details
### Database Schema Updates
- Added `threat_level` field to drone detections
- Added `estimated_distance` for distance tracking
- Added `requires_action` flag for security protocols
### API Enhancements
- Real-time threat assessment processing
- Enhanced alert message generation
- Threat-based filtering and alerting
### Frontend Integration
- Threat level indicators on maps and dashboards
- Color-coded threat visualization
- Enhanced alert rule configuration
## 📋 Recommended Configuration
### For Government Sites
```javascript
{
"min_threat_level": "high",
"max_distance": 200,
"cooldown_minutes": 2,
"channels": ["sms", "email", "webhook"]
}
```
### For Water Facilities
```javascript
{
"min_threat_level": "medium",
"max_distance": 500,
"cooldown_minutes": 10,
"channels": ["sms"]
}
```
### For Nuclear Facilities
```javascript
{
"min_threat_level": "medium",
"max_distance": 1000,
"cooldown_minutes": 0,
"channels": ["sms", "email", "webhook"],
"force_critical_alerts": true
}
```
## 🚀 Deployment Recommendations
1. **Test with Simulator**: Use the Python script to generate realistic test data
2. **Configure Threat Thresholds**: Set appropriate threat levels for each facility type
3. **Set Up Alert Channels**: Configure SMS, email, and webhook notifications
4. **Train Security Personnel**: Ensure staff understand threat levels and response protocols
5. **Monitor and Adjust**: Fine-tune threat thresholds based on real-world usage
## 📞 Emergency Response Integration
The system now supports:
- **Immediate escalation** for critical threats
- **Security protocol activation** based on threat levels
- **Multi-channel alerting** for redundancy
- **Real-time threat tracking** with distance monitoring
## 🔒 Security Compliance
Features designed for:
- **Government security standards**
- **Critical infrastructure protection**
- **Perimeter security monitoring**
- **Incident response protocols**
- **Audit and compliance logging**
This enhanced system provides enterprise-grade security monitoring specifically tailored for Swedish sensitive installations, with realistic testing capabilities and intelligent threat assessment.

209
docs/THREAT_ASSESSMENT.md Normal file
View File

@@ -0,0 +1,209 @@
# Threat Assessment and Security Features
## RSSI-Based Threat Classification
The drone detection system now includes intelligent threat assessment based on signal strength (RSSI) and drone type classification. This is specifically designed for government sites, water facilities, nuclear plants, and other sensitive Swedish installations.
### Threat Levels
The system automatically classifies detections into 5 threat levels:
#### 🔴 CRITICAL THREAT (RSSI ≥ -40 dBm)
- **Distance**: 0-50 meters from device
- **Action**: Immediate security response required
- **Description**: Drone within security perimeter
- **Alerts**: All available channels (SMS, email, webhook)
#### 🟠 HIGH THREAT (RSSI -55 to -40 dBm)
- **Distance**: 50-200 meters from device
- **Action**: Security response recommended
- **Description**: Drone approaching facility
- **Alerts**: SMS and email notifications
#### 🟡 MEDIUM THREAT (RSSI -70 to -55 dBm)
- **Distance**: 200m-1km from device
- **Action**: Enhanced monitoring
- **Description**: Drone in facility vicinity
- **Alerts**: SMS notifications (configurable)
#### 🟢 LOW THREAT (RSSI -85 to -70 dBm)
- **Distance**: 1-5 kilometers from device
- **Action**: Standard monitoring
- **Description**: Drone detected at distance
- **Alerts**: Log only (configurable)
#### ⚪ MONITORING (RSSI < -85 dBm)
- **Distance**: 5-15 kilometers from device
- **Action**: Passive monitoring
- **Description**: Long-range detection
- **Alerts**: Log only
### Drone Type Classification
Threat levels are adjusted based on drone type:
- **Type 0 (Consumer/Hobby)**: Standard threat assessment
- **Type 1 (Professional/Military)**: Escalated threat level
- **Type 2 (Racing/High-speed)**: Escalated if within close range
- **Type 3 (Unknown/Custom)**: Standard threat assessment
### Distance Calculation
The system estimates drone distance using RSSI with the formula:
```
Distance (m) = 10^((RSSI_at_1m - RSSI) / (10 * path_loss_exponent))
```
Where:
- `RSSI_at_1m = -30 dBm` (typical RSSI at 1 meter)
- `path_loss_exponent = 3` (outdoor environment with obstacles)
## Alert Rule Configuration
### Enhanced Alert Conditions
Alert rules now support advanced threat-based conditions:
```javascript
{
"conditions": {
"min_threat_level": "high", // Minimum threat level to trigger
"rssi_threshold": -55, // Minimum RSSI value
"max_distance": 200, // Maximum distance in meters
"drone_types": [0, 1, 2], // Allowed drone types
"device_ids": [1941875381] // Specific devices to monitor
},
"actions": {
"sms": true,
"phone_number": "+46701234567",
"email": true,
"channels": ["sms", "email"] // Alert channels
},
"cooldown_minutes": 5 // Cooldown between alerts
}
```
### Security Features for Sensitive Sites
#### Automatic Critical Threat Handling
- Critical threats (RSSI ≥ -40 dBm) automatically trigger all available alert channels
- Bypasses normal cooldown periods for immediate notification
- Includes estimated distance and threat description in alerts
#### Swedish Government Site Integration
The system is pre-configured with coordinates for:
- Government offices and Riksdag
- Water treatment facilities
- Nuclear power plants
- Military installations
- Major airports
## Python Simulation Script
### Swedish Drone Detection Simulator
The included `drone_simulator.py` script generates realistic drone detection data with Swedish coordinates:
```bash
# Install dependencies
pip install -r requirements.txt
# Run basic simulation
python drone_simulator.py
# Custom simulation parameters
python drone_simulator.py --devices 10 --detection-interval 30 --duration 7200
# List available Swedish locations
python drone_simulator.py --list-locations
```
### Simulation Features
- **Realistic RSSI Calculation**: Based on actual distance and path loss
- **Threat-Based Scenarios**: Different probability weights for each threat level
- **Swedish Coordinates**: Real coordinates for sensitive facilities
- **Multiple Device Types**: Government, water, nuclear, military, airport sites
- **Continuous Heartbeats**: Device health monitoring
- **Battery Simulation**: Realistic battery drain and status changes
### Threat Scenario Probabilities
- **Low Threat**: 70% (5-15km range, RSSI -90 to -70 dBm)
- **Medium Threat**: 20% (200m-5km range, RSSI -70 to -55 dBm)
- **High Threat**: 8% (50-200m range, RSSI -55 to -40 dBm)
- **Critical Threat**: 2% (0-50m range, RSSI -40 to -25 dBm)
## API Enhancements
### Detection Response Format
The API now returns threat assessment data:
```json
{
"id": "uuid",
"device_id": 1941875381,
"drone_id": 1001,
"rssi": -45,
"threat_level": "high",
"estimated_distance": 150,
"requires_action": true,
"geo_lat": 59.3293,
"geo_lon": 18.0686,
"timestamp": "2025-08-16T10:30:00Z"
}
```
### Enhanced Alert Messages
SMS alerts now include comprehensive threat information:
```
🚨 SECURITY ALERT 🚨
THREAT LEVEL: HIGH
HIGH THREAT: Drone approaching facility (50-200m)
📍 LOCATION: Riksdag Stockholm
🔧 DEVICE: SecureGuard-001
📏 DISTANCE: ~150m
📶 SIGNAL: -45 dBm
🚁 DRONE TYPE: Professional/Military
⏰ TIME: 2025-08-16 10:30:00
⚠️ IMMEDIATE ACTION REQUIRED
Security personnel should respond immediately.
```
## Database Schema Updates
New fields added to `DroneDetection` model:
- `threat_level`: ENUM('monitoring', 'low', 'medium', 'high', 'critical')
- `estimated_distance`: INTEGER (meters)
- `requires_action`: BOOLEAN
## Security Recommendations
### For Government Sites
- Set `min_threat_level` to "high" for critical facilities
- Use multiple alert channels for redundancy
- Configure short cooldown periods (2-5 minutes)
- Monitor all drone types including consumer drones
### For Water Facilities
- Set `min_threat_level` to "medium" for early warning
- Focus on perimeter monitoring (max_distance: 500m)
- Longer cooldown periods acceptable (10-15 minutes)
### For Nuclear Facilities
- Set `min_threat_level` to "medium" with escalation to "critical"
- Immediate response required for high/critical threats
- No cooldown for critical threats
- Monitor professional/military drone types with high priority
### For Military Installations
- Maximum security configuration
- All threat levels trigger alerts
- Multiple redundant alert channels
- Real-time monitoring and response protocols

379
drone_simulator.py Normal file
View File

@@ -0,0 +1,379 @@
#!/usr/bin/env python3
"""
Swedish Drone Detection Simulator
This script simulates drone detection data for testing the security monitoring system.
It generates realistic detection data with Swedish coordinates for government sites,
water facilities, and sensitive areas.
Usage:
python drone_simulator.py --help
python drone_simulator.py --devices 5 --interval 30 --duration 3600
"""
import requests
import time
import random
import json
import argparse
import threading
from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import List, Tuple
import math
# Swedish coordinates for various sensitive locations
SWEDISH_LOCATIONS = {
"government_sites": [
{"name": "Riksdag Stockholm", "lat": 59.3275, "lon": 18.0644},
{"name": "Government Offices Stockholm", "lat": 59.3320, "lon": 18.0648},
{"name": "Swedish Armed Forces HQ", "lat": 59.3242, "lon": 18.0969},
{"name": "SÄPO Headquarters", "lat": 59.3370, "lon": 18.0585},
{"name": "Royal Palace Stockholm", "lat": 59.3267, "lon": 18.0717},
],
"water_facilities": [
{"name": "Norsborg Water Treatment", "lat": 59.2766, "lon": 17.8217},
{"name": "Lovö Water Treatment", "lat": 59.3508, "lon": 17.8367},
{"name": "Göteborg Water Works", "lat": 57.6950, "lon": 11.9850},
{"name": "Malmö Water Treatment", "lat": 55.5833, "lon": 12.9833},
{"name": "Uppsala Water Plant", "lat": 59.8586, "lon": 17.6389},
],
"nuclear_facilities": [
{"name": "Forsmark Nuclear Plant", "lat": 60.4017, "lon": 18.1753},
{"name": "Oskarshamn Nuclear Plant", "lat": 57.4167, "lon": 16.6667},
{"name": "Ringhals Nuclear Plant", "lat": 57.2603, "lon": 12.1086},
],
"airports": [
{"name": "Arlanda Airport", "lat": 59.6519, "lon": 17.9186},
{"name": "Landvetter Airport", "lat": 57.6628, "lon": 12.2944},
{"name": "Kastrup Airport (Copenhagen)", "lat": 55.6181, "lon": 12.6563},
{"name": "Sturup Airport", "lat": 55.5264, "lon": 13.3761},
],
"military_bases": [
{"name": "Karlsborg Fortress", "lat": 58.5342, "lon": 14.5219},
{"name": "Boden Fortress", "lat": 65.8250, "lon": 21.6889},
{"name": "Gotland Regiment", "lat": 57.6364, "lon": 18.2944},
{"name": "Amf 1 Livgardet", "lat": 59.4017, "lon": 17.9439},
]
}
@dataclass
class DroneDevice:
"""Represents a drone detection device"""
device_id: int
name: str
location: str
lat: float
lon: float
category: str
last_heartbeat: float = 0
battery_level: int = 100
signal_strength: int = -45
temperature: float = 20.0
status: str = "active"
@dataclass
class DroneDetection:
"""Represents a drone detection event"""
device_id: int
geo_lat: float
geo_lon: float
device_timestamp: int
drone_type: int
rssi: int
freq: int
drone_id: int
class SwedishDroneSimulator:
"""Simulates drone detection events for Swedish security installations"""
def __init__(self, api_base_url: str = "http://localhost:3001/api"):
self.api_base_url = api_base_url
self.devices: List[DroneDevice] = []
self.running = False
self.threat_scenarios = {
"low_threat": {"probability": 0.7, "rssi_range": (-90, -70), "distance_km": (5, 15)},
"medium_threat": {"probability": 0.2, "rssi_range": (-70, -55), "distance_km": (0.2, 5)},
"high_threat": {"probability": 0.08, "rssi_range": (-55, -40), "distance_km": (0.05, 0.2)},
"critical_threat": {"probability": 0.02, "rssi_range": (-40, -25), "distance_km": (0, 0.05)}
}
def generate_devices(self, num_devices: int) -> List[DroneDevice]:
"""Generate drone detection devices at Swedish sensitive locations"""
devices = []
device_id_base = 1941875380
all_locations = []
for category, locations in SWEDISH_LOCATIONS.items():
for loc in locations:
all_locations.append((category, loc))
# Select random locations
selected_locations = random.sample(all_locations, min(num_devices, len(all_locations)))
for i, (category, location) in enumerate(selected_locations):
device = DroneDevice(
device_id=device_id_base + i + 1,
name=f"SecureGuard-{i+1:03d}",
location=location["name"],
lat=location["lat"],
lon=location["lon"],
category=category,
battery_level=random.randint(75, 100),
signal_strength=random.randint(-60, -30),
temperature=random.uniform(15, 30)
)
devices.append(device)
self.devices = devices
return devices
def calculate_threat_based_rssi(self, distance_km: float, base_rssi: int = -30) -> int:
"""Calculate RSSI based on distance for realistic threat simulation"""
# Free space path loss formula adapted for drone detection
# RSSI = base_rssi - 20*log10(distance_m) - path_loss_factor
distance_m = distance_km * 1000
if distance_m < 1:
distance_m = 1
path_loss = 20 * math.log10(distance_m) + random.randint(5, 15)
rssi = base_rssi - int(path_loss)
# Clamp to realistic RSSI range
return max(-100, min(-25, rssi))
def generate_detection_near_device(self, device: DroneDevice, threat_level: str = "low_threat") -> DroneDetection:
"""Generate a drone detection near a specific device with threat-based parameters"""
threat_config = self.threat_scenarios[threat_level]
# Generate distance based on threat level
min_dist, max_dist = threat_config["distance_km"]
distance_km = random.uniform(min_dist, max_dist)
# Calculate realistic RSSI based on distance
rssi = self.calculate_threat_based_rssi(distance_km)
# Ensure RSSI is within threat level range
rssi_min, rssi_max = threat_config["rssi_range"]
rssi = max(rssi_min, min(rssi_max, rssi))
# Generate coordinates near device (within distance)
lat_offset = (random.random() - 0.5) * (distance_km / 111.0) * 2 # ~111km per degree
lon_offset = (random.random() - 0.5) * (distance_km / (111.0 * math.cos(math.radians(device.lat)))) * 2
detection_lat = device.lat + lat_offset
detection_lon = device.lon + lon_offset
# Drone type based on threat level
if threat_level == "critical_threat":
drone_type = 1 # Professional/Military
elif threat_level == "high_threat":
drone_type = random.choice([1, 2]) # Professional or Racing
else:
drone_type = random.choice([0, 0, 0, 1, 2]) # Mostly consumer
return DroneDetection(
device_id=device.device_id,
geo_lat=round(detection_lat, 6),
geo_lon=round(detection_lon, 6),
device_timestamp=int(time.time()),
drone_type=drone_type,
rssi=rssi,
freq=random.choice([20, 22, 24, 25, 27, 58, 915, 2400, 2450, 5800]), # Common drone frequencies
drone_id=random.randint(1000, 9999)
)
def select_threat_level(self) -> str:
"""Select threat level based on probability weights for realistic scenarios"""
rand = random.random()
cumulative = 0
for threat_level, config in self.threat_scenarios.items():
cumulative += config["probability"]
if rand <= cumulative:
return threat_level
return "low_threat"
def send_detection(self, detection: DroneDetection) -> bool:
"""Send drone detection to the API"""
try:
payload = {
"device_id": detection.device_id,
"geo_lat": detection.geo_lat,
"geo_lon": detection.geo_lon,
"device_timestamp": detection.device_timestamp,
"drone_type": detection.drone_type,
"rssi": detection.rssi,
"freq": detection.freq,
"drone_id": detection.drone_id
}
response = requests.post(
f"{self.api_base_url}/detections",
json=payload,
headers={"Content-Type": "application/json"},
timeout=10
)
if response.status_code == 201:
threat_level = "CRITICAL" if detection.rssi >= -40 else "HIGH" if detection.rssi >= -55 else "MEDIUM" if detection.rssi >= -70 else "LOW"
device = next((d for d in self.devices if d.device_id == detection.device_id), None)
location_name = device.location if device else "Unknown"
print(f"🚨 {threat_level} THREAT DETECTION:")
print(f" Location: {location_name}")
print(f" Device: {detection.device_id}")
print(f" RSSI: {detection.rssi} dBm")
print(f" Drone Type: {detection.drone_type}")
print(f" Est. Distance: ~{round(10**(((-30) - detection.rssi) / (10 * 3)))}m")
print(f" Coordinates: {detection.geo_lat}, {detection.geo_lon}")
print()
return True
else:
print(f"❌ Failed to send detection: {response.status_code} - {response.text}")
return False
except Exception as e:
print(f"❌ Error sending detection: {e}")
return False
def send_heartbeat(self, device: DroneDevice) -> bool:
"""Send device heartbeat to the API"""
try:
# Simulate device status changes
if random.random() < 0.05: # 5% chance of status change
device.battery_level = max(10, device.battery_level - random.randint(1, 5))
device.signal_strength = random.randint(-65, -25)
device.temperature = random.uniform(10, 35)
payload = {
"type": "heartbeat",
"key": f"device_{device.device_id}_key",
"battery_level": device.battery_level,
"signal_strength": device.signal_strength,
"temperature": device.temperature
}
response = requests.post(
f"{self.api_base_url}/heartbeat",
json=payload,
headers={"Content-Type": "application/json"},
timeout=10
)
if response.status_code == 200:
device.last_heartbeat = time.time()
status_icon = "🟢" if device.battery_level > 30 else "🟡" if device.battery_level > 15 else "🔴"
print(f"{status_icon} Heartbeat: {device.name} ({device.location}) - Battery: {device.battery_level}%")
return True
else:
print(f"❌ Failed to send heartbeat for device {device.device_id}: {response.status_code}")
return False
except Exception as e:
print(f"❌ Error sending heartbeat for device {device.device_id}: {e}")
return False
def run_simulation(self, detection_interval: int = 60, heartbeat_interval: int = 30, duration: int = 3600):
"""Run the complete simulation"""
print("🇸🇪 Swedish Drone Detection Security Simulator")
print("=" * 50)
print(f"📊 Simulation Parameters:")
print(f" • Devices: {len(self.devices)}")
print(f" • Detection interval: {detection_interval}s")
print(f" • Heartbeat interval: {heartbeat_interval}s")
print(f" • Duration: {duration}s ({duration//60} minutes)")
print(f" • API Base URL: {self.api_base_url}")
print()
# Display device locations
print("📍 Monitoring Locations:")
for device in self.devices:
print(f"{device.name}: {device.location} ({device.category.replace('_', ' ').title()})")
print()
self.running = True
start_time = time.time()
last_detection_time = 0
last_heartbeat_time = 0
print("🚀 Starting simulation...\n")
try:
while self.running and (time.time() - start_time) < duration:
current_time = time.time()
# Send heartbeats
if current_time - last_heartbeat_time >= heartbeat_interval:
for device in self.devices:
self.send_heartbeat(device)
last_heartbeat_time = current_time
print()
# Generate detections
if current_time - last_detection_time >= detection_interval:
# Simulate detection probability (not every interval)
if random.random() < 0.4: # 40% chance of detection
device = random.choice(self.devices)
threat_level = self.select_threat_level()
detection = self.generate_detection_near_device(device, threat_level)
self.send_detection(detection)
last_detection_time = current_time
# Sleep for a short interval
time.sleep(1)
except KeyboardInterrupt:
print("\n🛑 Simulation stopped by user")
print(f"\n✅ Simulation completed. Duration: {int(time.time() - start_time)}s")
def main():
parser = argparse.ArgumentParser(description="Swedish Drone Detection Security Simulator")
parser.add_argument("--devices", type=int, default=5, help="Number of devices to simulate (default: 5)")
parser.add_argument("--detection-interval", type=int, default=60, help="Detection interval in seconds (default: 60)")
parser.add_argument("--heartbeat-interval", type=int, default=30, help="Heartbeat interval in seconds (default: 30)")
parser.add_argument("--duration", type=int, default=3600, help="Simulation duration in seconds (default: 3600)")
parser.add_argument("--api-url", default="http://localhost:3001/api", help="API base URL (default: http://localhost:3001/api)")
parser.add_argument("--list-locations", action="store_true", help="List all available Swedish locations and exit")
args = parser.parse_args()
if args.list_locations:
print("🇸🇪 Available Swedish Security Monitoring Locations:")
print("=" * 60)
for category, locations in SWEDISH_LOCATIONS.items():
print(f"\n{category.replace('_', ' ').title()}:")
for i, loc in enumerate(locations, 1):
print(f" {i}. {loc['name']} ({loc['lat']}, {loc['lon']})")
return
# Initialize simulator
simulator = SwedishDroneSimulator(args.api_url)
# Generate devices
devices = simulator.generate_devices(args.devices)
# Test API connectivity
try:
response = requests.get(f"{args.api_url}/dashboard/stats", timeout=5)
if response.status_code != 200:
print(f"⚠️ Warning: API test failed ({response.status_code}). Proceeding anyway...")
except Exception as e:
print(f"⚠️ Warning: Could not connect to API at {args.api_url}")
print(f" Make sure the backend server is running!")
print(f" Error: {e}")
print()
# Run simulation
simulator.run_simulation(
detection_interval=args.detection_interval,
heartbeat_interval=args.heartbeat_interval,
duration=args.duration
)
if __name__ == "__main__":
main()

37
package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "drone-detection-system",
"version": "1.0.0",
"description": "Real-time drone detection and monitoring system",
"main": "server/index.js",
"scripts": {
"install:all": "npm install && cd server && npm install && cd ../client && npm install && cd ..",
"install:server": "cd server && npm install",
"install:client": "cd client && npm install",
"dev": "concurrently \"npm run server:dev\" \"npm run client:dev\"",
"server:dev": "cd server && npm run dev",
"server:start": "cd server && npm start",
"client:dev": "cd client && npm run dev",
"client:build": "cd client && npm run build",
"client:preview": "cd client && npm run preview",
"db:setup": "cd server && node scripts/setup-database.js",
"db:reset": "cd server && node scripts/setup-database.js",
"build": "npm run client:build",
"start": "npm run server:start",
"setup": "npm run install:all && npm run db:setup",
"docker:build": "docker-compose build",
"docker:up": "docker-compose up -d",
"docker:down": "docker-compose down",
"docker:logs": "docker-compose logs -f",
"docker:restart": "docker-compose restart",
"docker:clean": "docker-compose down -v && docker system prune -f",
"docker:prod": "docker-compose --profile production up -d",
"docker:simulate": "docker-compose --profile simulation up -d",
"docker:backup": "docker-compose exec postgres pg_dump -U postgres drone_detection > backup-$(date +%Y%m%d-%H%M%S).sql"
},
"keywords": ["drone", "detection", "monitoring", "iot", "security"],
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"concurrently": "^7.6.0"
}
}

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
requests>=2.28.0

102
server/.dockerignore Normal file
View File

@@ -0,0 +1,102 @@
# Node.js
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
# nyc test coverage
.nyc_output
# Dependency directories
node_modules/
# 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
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Development files
.git
.gitignore
README.md
Dockerfile
.dockerignore
# IDE files
.vscode/
.idea/
*.swp
*.swo
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

29
server/.env.example Normal file
View File

@@ -0,0 +1,29 @@
# Environment Configuration
NODE_ENV=development
PORT=3001
# Database Configuration
DB_HOST=localhost
DB_PORT=5432
DB_NAME=drone_detection
DB_USER=postgres
DB_PASSWORD=password
# JWT Secret
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
# Twilio Configuration (for SMS alerts)
TWILIO_ACCOUNT_SID=your-twilio-account-sid
TWILIO_AUTH_TOKEN=your-twilio-auth-token
TWILIO_PHONE_NUMBER=your-twilio-phone-number
# Alert Configuration
SMS_ALERTS_ENABLED=true
EMAIL_ALERTS_ENABLED=false
# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
# CORS Configuration
CORS_ORIGIN=http://localhost:3000

49
server/Dockerfile Normal file
View File

@@ -0,0 +1,49 @@
# Backend Dockerfile for Drone Detection System
FROM node:18-alpine AS base
# Install system dependencies
RUN apk add --no-cache \
python3 \
make \
g++ \
curl \
dumb-init
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production && \
npm cache clean --force
# Copy application code
COPY . .
# Create logs directory
RUN mkdir -p logs
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Set ownership
RUN chown -R nodejs:nodejs /app
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 3001
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:3001/api/health || exit 1
# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
# Start the application
CMD ["npm", "start"]

95
server/index.js Normal file
View File

@@ -0,0 +1,95 @@
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const { createServer } = require('http');
const { Server } = require('socket.io');
require('dotenv').config();
const { sequelize } = require('./models');
const routes = require('./routes');
const { initializeSocketHandlers } = require('./services/socketService');
const errorHandler = require('./middleware/errorHandler');
const app = express();
const server = createServer(app);
const io = new Server(server, {
cors: {
origin: process.env.CORS_ORIGIN || "http://localhost:3000",
methods: ["GET", "POST", "PUT", "DELETE"]
}
});
// Rate limiting
const limiter = rateLimit({
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minutes
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100,
message: 'Too many requests from this IP, please try again later.'
});
// Middleware
app.use(helmet());
app.use(compression());
app.use(morgan('combined'));
app.use(cors({
origin: process.env.CORS_ORIGIN || "http://localhost:3000",
credentials: true
}));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.use('/api/', limiter);
// Make io available to routes
app.use((req, res, next) => {
req.io = io;
next();
});
// Routes
app.use('/api', routes);
// Health check endpoints
app.get('/health', (req, res) => {
res.status(200).json({
status: 'OK',
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV
});
});
app.use('/api/health', require('./routes/health'));
// Error handling
app.use(errorHandler);
// Socket.IO initialization
initializeSocketHandlers(io);
const PORT = process.env.PORT || 3001;
// Database connection and server startup
async function startServer() {
try {
await sequelize.authenticate();
console.log('Database connected successfully.');
if (process.env.NODE_ENV !== 'production') {
await sequelize.sync({ alter: true });
console.log('Database synchronized.');
}
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`Environment: ${process.env.NODE_ENV}`);
});
} catch (error) {
console.error('Unable to start server:', error);
process.exit(1);
}
}
startServer();
module.exports = { app, server, io };

63
server/middleware/auth.js Normal file
View File

@@ -0,0 +1,63 @@
const jwt = require('jsonwebtoken');
const { User } = require('../models');
async function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({
success: false,
message: 'Access token required'
});
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByPk(decoded.userId, {
attributes: ['id', 'username', 'email', 'role', 'is_active']
});
if (!user || !user.is_active) {
return res.status(401).json({
success: false,
message: 'Invalid or inactive user'
});
}
req.user = user;
next();
} catch (error) {
console.error('Token verification error:', error);
return res.status(403).json({
success: false,
message: 'Invalid or expired token'
});
}
}
function requireRole(roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Authentication required'
});
}
const userRoles = Array.isArray(roles) ? roles : [roles];
if (!userRoles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: 'Insufficient permissions'
});
}
next();
};
}
module.exports = {
authenticateToken,
requireRole
};

View File

@@ -0,0 +1,57 @@
function errorHandler(error, req, res, next) {
console.error('Error occurred:', error);
// Default error response
let statusCode = 500;
let message = 'Internal server error';
let details = null;
// Handle specific error types
if (error.name === 'ValidationError') {
statusCode = 400;
message = 'Validation error';
details = error.details || error.message;
} else if (error.name === 'UnauthorizedError') {
statusCode = 401;
message = 'Unauthorized';
} else if (error.name === 'SequelizeValidationError') {
statusCode = 400;
message = 'Database validation error';
details = error.errors.map(err => ({
field: err.path,
message: err.message
}));
} else if (error.name === 'SequelizeUniqueConstraintError') {
statusCode = 409;
message = 'Resource already exists';
details = error.errors.map(err => ({
field: err.path,
message: err.message
}));
} else if (error.name === 'SequelizeForeignKeyConstraintError') {
statusCode = 400;
message = 'Invalid reference';
} else if (error.status) {
statusCode = error.status;
message = error.message;
}
// Send error response
const response = {
success: false,
message,
timestamp: new Date().toISOString()
};
// Include error details in development
if (process.env.NODE_ENV === 'development') {
response.error = details || error.message;
response.stack = error.stack;
} else if (details) {
response.details = details;
}
res.status(statusCode).json(response);
}
module.exports = errorHandler;

View File

@@ -0,0 +1,30 @@
function validateRequest(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true
});
if (error) {
const errorDetails = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message,
value: detail.context.value
}));
return res.status(400).json({
success: false,
message: 'Validation error',
errors: errorDetails
});
}
// Replace req.body with validated and sanitized data
req.body = value;
next();
};
}
module.exports = {
validateRequest
};

109
server/models/AlertLog.js Normal file
View File

@@ -0,0 +1,109 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const AlertLog = sequelize.define('AlertLog', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
alert_rule_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'alert_rules',
key: 'id'
}
},
detection_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'drone_detections',
key: 'id'
}
},
alert_type: {
type: DataTypes.ENUM('sms', 'email', 'webhook', 'push'),
allowNull: false
},
recipient: {
type: DataTypes.STRING,
allowNull: false,
comment: 'Phone number, email, or webhook URL'
},
message: {
type: DataTypes.TEXT,
allowNull: false,
comment: 'Alert message content'
},
status: {
type: DataTypes.ENUM('pending', 'sent', 'failed', 'delivered'),
defaultValue: 'pending'
},
sent_at: {
type: DataTypes.DATE,
allowNull: true
},
delivered_at: {
type: DataTypes.DATE,
allowNull: true
},
error_message: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Error message if alert failed'
},
external_id: {
type: DataTypes.STRING,
allowNull: true,
comment: 'External service message ID (Twilio SID, etc.)'
},
cost: {
type: DataTypes.DECIMAL(6, 4),
allowNull: true,
comment: 'Cost of sending the alert (for SMS, etc.)'
},
retry_count: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: 'Number of retry attempts'
},
priority: {
type: DataTypes.ENUM('low', 'medium', 'high', 'critical'),
defaultValue: 'medium'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'alert_logs',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{
fields: ['alert_rule_id']
},
{
fields: ['detection_id']
},
{
fields: ['status']
},
{
fields: ['sent_at']
},
{
fields: ['alert_type', 'status']
}
]
});
return AlertLog;
};

124
server/models/AlertRule.js Normal file
View File

@@ -0,0 +1,124 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const AlertRule = sequelize.define('AlertRule', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
user_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
name: {
type: DataTypes.STRING,
allowNull: false,
comment: 'Human-readable name for the alert rule'
},
description: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Description of what triggers this alert'
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
device_ids: {
type: DataTypes.JSON,
allowNull: true,
comment: 'Array of device IDs to monitor (null = all devices)'
},
drone_types: {
type: DataTypes.JSON,
allowNull: true,
comment: 'Array of drone types to alert on (null = all types)'
},
min_rssi: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Minimum RSSI threshold for alert'
},
max_rssi: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Maximum RSSI threshold for alert'
},
frequency_ranges: {
type: DataTypes.JSON,
allowNull: true,
comment: 'Array of frequency ranges to monitor [{min: 20, max: 30}]'
},
time_window: {
type: DataTypes.INTEGER,
defaultValue: 300,
comment: 'Time window in seconds to group detections'
},
min_detections: {
type: DataTypes.INTEGER,
defaultValue: 1,
comment: 'Minimum number of detections in time window to trigger alert'
},
cooldown_period: {
type: DataTypes.INTEGER,
defaultValue: 600,
comment: 'Cooldown period in seconds between alerts for same drone'
},
alert_channels: {
type: DataTypes.JSON,
defaultValue: ['sms'],
comment: 'Array of alert channels: sms, email, webhook'
},
webhook_url: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Webhook URL for custom integrations'
},
active_hours: {
type: DataTypes.JSON,
allowNull: true,
comment: 'Active hours for alerts {start: "09:00", end: "17:00"}'
},
active_days: {
type: DataTypes.JSON,
defaultValue: [1, 2, 3, 4, 5, 6, 7],
comment: 'Active days of week (1=Monday, 7=Sunday)'
},
priority: {
type: DataTypes.ENUM('low', 'medium', 'high', 'critical'),
defaultValue: 'medium',
comment: 'Alert priority level'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'alert_rules',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{
fields: ['user_id']
},
{
fields: ['is_active']
},
{
fields: ['priority']
}
]
});
return AlertRule;
};

88
server/models/Device.js Normal file
View File

@@ -0,0 +1,88 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const Device = sequelize.define('Device', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
allowNull: false,
comment: 'Unique device identifier from hardware'
},
name: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Human-readable device name'
},
geo_lat: {
type: DataTypes.DECIMAL(10, 8),
allowNull: true,
comment: 'Device latitude coordinate'
},
geo_lon: {
type: DataTypes.DECIMAL(11, 8),
allowNull: true,
comment: 'Device longitude coordinate'
},
location_description: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Human-readable location description'
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true,
comment: 'Whether the device is currently active'
},
last_heartbeat: {
type: DataTypes.DATE,
allowNull: true,
comment: 'Timestamp of last heartbeat received'
},
heartbeat_interval: {
type: DataTypes.INTEGER,
defaultValue: 300, // 5 minutes
comment: 'Expected heartbeat interval in seconds'
},
firmware_version: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Device firmware version'
},
installation_date: {
type: DataTypes.DATE,
allowNull: true,
comment: 'When the device was installed'
},
notes: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Additional notes about the device'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'devices',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{
fields: ['geo_lat', 'geo_lon']
},
{
fields: ['last_heartbeat']
},
{
fields: ['is_active']
}
]
});
return Device;
};

View File

@@ -0,0 +1,120 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const DroneDetection = sequelize.define('DroneDetection', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
device_id: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'devices',
key: 'id'
},
comment: 'ID of the detecting device'
},
drone_id: {
type: DataTypes.INTEGER,
allowNull: false,
comment: 'Detected drone identifier'
},
drone_type: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Type of drone detected (0=unknown, 1=commercial, 2=racing, etc.)'
},
rssi: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Received Signal Strength Indicator'
},
freq: {
type: DataTypes.INTEGER,
allowNull: false,
comment: 'Frequency detected'
},
geo_lat: {
type: DataTypes.DECIMAL(10, 8),
allowNull: true,
comment: 'Latitude where detection occurred'
},
geo_lon: {
type: DataTypes.DECIMAL(11, 8),
allowNull: true,
comment: 'Longitude where detection occurred'
},
device_timestamp: {
type: DataTypes.BIGINT,
allowNull: true,
comment: 'Unix timestamp from the device'
},
server_timestamp: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
comment: 'When the detection was received by server'
},
confidence_level: {
type: DataTypes.DECIMAL(3, 2),
allowNull: true,
comment: 'Confidence level of detection (0.00-1.00)'
},
signal_duration: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Duration of signal in milliseconds'
},
processed: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: 'Whether this detection has been processed for alerts'
},
threat_level: {
type: DataTypes.ENUM('monitoring', 'low', 'medium', 'high', 'critical'),
allowNull: true,
comment: 'Assessed threat level based on RSSI and drone type'
},
estimated_distance: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Estimated distance to drone in meters'
},
requires_action: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: 'Whether this detection requires immediate security action'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'drone_detections',
timestamps: true,
createdAt: 'created_at',
updatedAt: false,
indexes: [
{
fields: ['device_id']
},
{
fields: ['drone_id']
},
{
fields: ['server_timestamp']
},
{
fields: ['processed']
},
{
fields: ['device_id', 'drone_id', 'server_timestamp']
}
]
});
return DroneDetection;
};

View File

@@ -0,0 +1,82 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const Heartbeat = sequelize.define('Heartbeat', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
device_id: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'devices',
key: 'id'
},
comment: 'ID of the device sending heartbeat'
},
device_key: {
type: DataTypes.STRING,
allowNull: false,
comment: 'Unique key of the sensor from heartbeat message'
},
signal_strength: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Signal strength at time of heartbeat'
},
battery_level: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Battery level percentage (0-100)'
},
temperature: {
type: DataTypes.DECIMAL(4, 1),
allowNull: true,
comment: 'Device temperature in Celsius'
},
uptime: {
type: DataTypes.BIGINT,
allowNull: true,
comment: 'Device uptime in seconds'
},
memory_usage: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Memory usage percentage'
},
firmware_version: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Firmware version reported in heartbeat'
},
received_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
comment: 'When heartbeat was received by server'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'heartbeats',
timestamps: true,
createdAt: 'created_at',
updatedAt: false,
indexes: [
{
fields: ['device_id']
},
{
fields: ['received_at']
},
{
fields: ['device_id', 'received_at']
}
]
});
return Heartbeat;
};

98
server/models/User.js Normal file
View File

@@ -0,0 +1,98 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const User = sequelize.define('User', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
len: [3, 50]
}
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password_hash: {
type: DataTypes.STRING,
allowNull: false
},
first_name: {
type: DataTypes.STRING,
allowNull: true
},
last_name: {
type: DataTypes.STRING,
allowNull: true
},
phone_number: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Phone number for SMS alerts (include country code)'
},
role: {
type: DataTypes.ENUM('admin', 'operator', 'viewer'),
defaultValue: 'viewer',
comment: 'User role for permission management'
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
sms_alerts_enabled: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: 'Whether user wants to receive SMS alerts'
},
email_alerts_enabled: {
type: DataTypes.BOOLEAN,
defaultValue: true,
comment: 'Whether user wants to receive email alerts'
},
last_login: {
type: DataTypes.DATE,
allowNull: true
},
timezone: {
type: DataTypes.STRING,
defaultValue: 'UTC',
comment: 'User timezone for alert scheduling'
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'users',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{
fields: ['email']
},
{
fields: ['username']
},
{
fields: ['phone_number']
}
]
});
return User;
};

54
server/models/index.js Normal file
View File

@@ -0,0 +1,54 @@
const { Sequelize } = require('sequelize');
require('dotenv').config();
const sequelize = new Sequelize(
process.env.DB_NAME || 'drone_detection',
process.env.DB_USER || 'postgres',
process.env.DB_PASSWORD || 'password',
{
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: process.env.NODE_ENV === 'development' ? console.log : false,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
}
);
// Import models
const Device = require('./Device')(sequelize);
const DroneDetection = require('./DroneDetection')(sequelize);
const Heartbeat = require('./Heartbeat')(sequelize);
const User = require('./User')(sequelize);
const AlertRule = require('./AlertRule')(sequelize);
const AlertLog = require('./AlertLog')(sequelize);
// Define associations
Device.hasMany(DroneDetection, { foreignKey: 'device_id', as: 'detections' });
DroneDetection.belongsTo(Device, { foreignKey: 'device_id', as: 'device' });
Device.hasMany(Heartbeat, { foreignKey: 'device_id', as: 'heartbeats' });
Heartbeat.belongsTo(Device, { foreignKey: 'device_id', as: 'device' });
User.hasMany(AlertRule, { foreignKey: 'user_id', as: 'alertRules' });
AlertRule.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
AlertRule.hasMany(AlertLog, { foreignKey: 'alert_rule_id', as: 'logs' });
AlertLog.belongsTo(AlertRule, { foreignKey: 'alert_rule_id', as: 'rule' });
DroneDetection.hasMany(AlertLog, { foreignKey: 'detection_id', as: 'alerts' });
AlertLog.belongsTo(DroneDetection, { foreignKey: 'detection_id', as: 'detection' });
module.exports = {
sequelize,
Device,
DroneDetection,
Heartbeat,
User,
AlertRule,
AlertLog
};

35
server/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "drone-detection-server",
"version": "1.0.0",
"description": "Backend API for drone detection system",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"db:setup": "node scripts/setup-database.js",
"db:migrate": "node scripts/migrate.js",
"test": "jest"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"helmet": "^7.0.0",
"morgan": "^1.10.0",
"dotenv": "^16.3.1",
"pg": "^8.11.3",
"pg-hstore": "^2.3.4",
"sequelize": "^6.32.1",
"socket.io": "^4.7.2",
"twilio": "^4.14.0",
"joi": "^17.9.2",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.1",
"express-rate-limit": "^6.8.1",
"compression": "^1.7.4"
},
"devDependencies": {
"nodemon": "^3.0.1",
"jest": "^29.6.1",
"supertest": "^6.3.3"
}
}

311
server/routes/alert.js Normal file
View File

@@ -0,0 +1,311 @@
const express = require('express');
const router = express.Router();
const Joi = require('joi');
const { AlertRule, AlertLog, User } = require('../models');
const { validateRequest } = require('../middleware/validation');
const { authenticateToken, requireRole } = require('../middleware/auth');
const { Op } = require('sequelize');
// Validation schemas
const alertRuleSchema = Joi.object({
name: Joi.string().required(),
description: Joi.string().optional(),
device_ids: Joi.array().items(Joi.number().integer()).optional(),
drone_types: Joi.array().items(Joi.number().integer()).optional(),
min_rssi: Joi.number().integer().optional(),
max_rssi: Joi.number().integer().optional(),
frequency_ranges: Joi.array().items(Joi.object({
min: Joi.number().integer().required(),
max: Joi.number().integer().required()
})).optional(),
time_window: Joi.number().integer().min(60).max(3600).default(300),
min_detections: Joi.number().integer().min(1).default(1),
cooldown_period: Joi.number().integer().min(0).default(600),
alert_channels: Joi.array().items(Joi.string().valid('sms', 'email', 'webhook')).default(['sms']),
webhook_url: Joi.string().uri().optional(),
active_hours: Joi.object({
start: Joi.string().pattern(/^\d{2}:\d{2}$/).optional(),
end: Joi.string().pattern(/^\d{2}:\d{2}$/).optional()
}).optional(),
active_days: Joi.array().items(Joi.number().integer().min(1).max(7)).default([1,2,3,4,5,6,7]),
priority: Joi.string().valid('low', 'medium', 'high', 'critical').default('medium')
});
// GET /api/alerts/rules - Get alert rules for current user
router.get('/rules', authenticateToken, async (req, res) => {
try {
const { limit = 50, offset = 0, is_active } = req.query;
const whereClause = { user_id: req.user.id };
if (is_active !== undefined) whereClause.is_active = is_active === 'true';
const alertRules = await AlertRule.findAndCountAll({
where: whereClause,
limit: Math.min(parseInt(limit), 100),
offset: parseInt(offset),
order: [['created_at', 'DESC']]
});
res.json({
success: true,
data: alertRules.rows,
pagination: {
total: alertRules.count,
limit: parseInt(limit),
offset: parseInt(offset),
pages: Math.ceil(alertRules.count / parseInt(limit))
}
});
} catch (error) {
console.error('Error fetching alert rules:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch alert rules',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// POST /api/alerts/rules - Create new alert rule
router.post('/rules', authenticateToken, validateRequest(alertRuleSchema), async (req, res) => {
try {
const alertRule = await AlertRule.create({
...req.body,
user_id: req.user.id
});
res.status(201).json({
success: true,
data: alertRule,
message: 'Alert rule created successfully'
});
} catch (error) {
console.error('Error creating alert rule:', error);
res.status(500).json({
success: false,
message: 'Failed to create alert rule',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// PUT /api/alerts/rules/:id - Update alert rule
router.put('/rules/:id', authenticateToken, validateRequest(alertRuleSchema), async (req, res) => {
try {
const alertRule = await AlertRule.findOne({
where: {
id: req.params.id,
user_id: req.user.id
}
});
if (!alertRule) {
return res.status(404).json({
success: false,
message: 'Alert rule not found'
});
}
await alertRule.update(req.body);
res.json({
success: true,
data: alertRule,
message: 'Alert rule updated successfully'
});
} catch (error) {
console.error('Error updating alert rule:', error);
res.status(500).json({
success: false,
message: 'Failed to update alert rule',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// DELETE /api/alerts/rules/:id - Delete alert rule
router.delete('/rules/:id', authenticateToken, async (req, res) => {
try {
const alertRule = await AlertRule.findOne({
where: {
id: req.params.id,
user_id: req.user.id
}
});
if (!alertRule) {
return res.status(404).json({
success: false,
message: 'Alert rule not found'
});
}
await alertRule.destroy();
res.json({
success: true,
message: 'Alert rule deleted successfully'
});
} catch (error) {
console.error('Error deleting alert rule:', error);
res.status(500).json({
success: false,
message: 'Failed to delete alert rule',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/alerts/logs - Get alert logs for current user
router.get('/logs', authenticateToken, async (req, res) => {
try {
const {
limit = 50,
offset = 0,
status,
alert_type,
start_date,
end_date
} = req.query;
const whereClause = {};
if (status) whereClause.status = status;
if (alert_type) whereClause.alert_type = alert_type;
if (start_date || end_date) {
whereClause.created_at = {};
if (start_date) whereClause.created_at[Op.gte] = new Date(start_date);
if (end_date) whereClause.created_at[Op.lte] = new Date(end_date);
}
const alertLogs = await AlertLog.findAndCountAll({
where: whereClause,
include: [{
model: AlertRule,
as: 'rule',
where: { user_id: req.user.id },
attributes: ['id', 'name', 'priority']
}],
limit: Math.min(parseInt(limit), 200),
offset: parseInt(offset),
order: [['created_at', 'DESC']]
});
res.json({
success: true,
data: alertLogs.rows,
pagination: {
total: alertLogs.count,
limit: parseInt(limit),
offset: parseInt(offset),
pages: Math.ceil(alertLogs.count / parseInt(limit))
}
});
} catch (error) {
console.error('Error fetching alert logs:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch alert logs',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/alerts/stats - Get alert statistics for current user
router.get('/stats', authenticateToken, async (req, res) => {
try {
const { hours = 24 } = req.query;
const timeWindow = new Date(Date.now() - hours * 60 * 60 * 1000);
// Get user's alert rules
const userRuleIds = await AlertRule.findAll({
where: { user_id: req.user.id },
attributes: ['id']
}).then(rules => rules.map(rule => rule.id));
if (userRuleIds.length === 0) {
return res.json({
success: true,
data: {
total_alerts: 0,
sent_alerts: 0,
failed_alerts: 0,
pending_alerts: 0,
by_type: {},
by_status: {}
}
});
}
const [totalAlerts, alertsByStatus, alertsByType] = await Promise.all([
AlertLog.count({
where: {
alert_rule_id: { [Op.in]: userRuleIds },
created_at: { [Op.gte]: timeWindow }
}
}),
AlertLog.findAll({
where: {
alert_rule_id: { [Op.in]: userRuleIds },
created_at: { [Op.gte]: timeWindow }
},
attributes: [
'status',
[sequelize.fn('COUNT', '*'), 'count']
],
group: ['status'],
raw: true
}),
AlertLog.findAll({
where: {
alert_rule_id: { [Op.in]: userRuleIds },
created_at: { [Op.gte]: timeWindow }
},
attributes: [
'alert_type',
[sequelize.fn('COUNT', '*'), 'count']
],
group: ['alert_type'],
raw: true
})
]);
const statusCounts = alertsByStatus.reduce((acc, item) => {
acc[item.status] = parseInt(item.count);
return acc;
}, {});
const typeCounts = alertsByType.reduce((acc, item) => {
acc[item.alert_type] = parseInt(item.count);
return acc;
}, {});
res.json({
success: true,
data: {
total_alerts: totalAlerts,
sent_alerts: statusCounts.sent || 0,
failed_alerts: statusCounts.failed || 0,
pending_alerts: statusCounts.pending || 0,
by_type: typeCounts,
by_status: statusCounts,
time_window_hours: hours
}
});
} catch (error) {
console.error('Error fetching alert statistics:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch alert statistics',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
module.exports = router;

270
server/routes/dashboard.js Normal file
View File

@@ -0,0 +1,270 @@
const express = require('express');
const router = express.Router();
const { DroneDetection, Device, Heartbeat } = require('../models');
const { Op } = require('sequelize');
const { sequelize } = require('../models');
// GET /api/dashboard/overview - Get dashboard overview statistics
router.get('/overview', async (req, res) => {
try {
const { hours = 24 } = req.query;
const timeWindow = new Date(Date.now() - hours * 60 * 60 * 1000);
// Get basic statistics
const [
totalDevices,
activeDevices,
totalDetections,
recentDetections,
uniqueDronesDetected
] = await Promise.all([
Device.count(),
Device.count({ where: { is_active: true } }),
DroneDetection.count(),
DroneDetection.count({
where: { server_timestamp: { [Op.gte]: timeWindow } }
}),
DroneDetection.count({
where: { server_timestamp: { [Op.gte]: timeWindow } },
distinct: true,
col: 'drone_id'
})
]);
// Get device status breakdown
const devices = await Device.findAll({
attributes: ['id', 'last_heartbeat', 'heartbeat_interval', 'is_active']
});
const now = new Date();
let onlineDevices = 0;
let offlineDevices = 0;
devices.forEach(device => {
if (!device.is_active) return;
const timeSinceLastHeartbeat = device.last_heartbeat
? (now - new Date(device.last_heartbeat)) / 1000
: null;
const expectedInterval = device.heartbeat_interval || 300;
const isOnline = timeSinceLastHeartbeat && timeSinceLastHeartbeat < (expectedInterval * 2);
if (isOnline) {
onlineDevices++;
} else {
offlineDevices++;
}
});
// Get recent alerts count
// This would require AlertLog model which we haven't imported yet
// const recentAlerts = await AlertLog.count({
// where: { created_at: { [Op.gte]: timeWindow } }
// });
res.json({
success: true,
data: {
summary: {
total_devices: totalDevices,
active_devices: activeDevices,
online_devices: onlineDevices,
offline_devices: offlineDevices,
total_detections: totalDetections,
recent_detections: recentDetections,
unique_drones_detected: uniqueDronesDetected,
// recent_alerts: recentAlerts || 0,
time_window_hours: hours
},
device_status: {
total: totalDevices,
active: activeDevices,
online: onlineDevices,
offline: offlineDevices,
inactive: totalDevices - activeDevices
}
}
});
} catch (error) {
console.error('Error fetching dashboard overview:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch dashboard overview',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/dashboard/activity - Get recent activity feed
router.get('/activity', async (req, res) => {
try {
const { limit = 50, hours = 24 } = req.query;
const timeWindow = new Date(Date.now() - hours * 60 * 60 * 1000);
// Get recent detections with device info
const recentDetections = await DroneDetection.findAll({
where: { server_timestamp: { [Op.gte]: timeWindow } },
include: [{
model: Device,
as: 'device',
attributes: ['id', 'name', 'geo_lat', 'geo_lon', 'location_description']
}],
limit: Math.min(parseInt(limit), 200),
order: [['server_timestamp', 'DESC']]
});
// Get recent heartbeats
const recentHeartbeats = await Heartbeat.findAll({
where: { received_at: { [Op.gte]: timeWindow } },
include: [{
model: Device,
as: 'device',
attributes: ['id', 'name', 'geo_lat', 'geo_lon']
}],
limit: Math.min(parseInt(limit), 50),
order: [['received_at', 'DESC']]
});
// Combine and sort activities
const activities = [
...recentDetections.map(detection => ({
type: 'detection',
timestamp: detection.server_timestamp,
data: {
detection_id: detection.id,
device_id: detection.device_id,
device_name: detection.device.name || `Device ${detection.device_id}`,
drone_id: detection.drone_id,
drone_type: detection.drone_type,
rssi: detection.rssi,
freq: detection.freq,
location: detection.device.location_description ||
`${detection.device.geo_lat}, ${detection.device.geo_lon}`
}
})),
...recentHeartbeats.map(heartbeat => ({
type: 'heartbeat',
timestamp: heartbeat.received_at,
data: {
device_id: heartbeat.device_id,
device_name: heartbeat.device.name || `Device ${heartbeat.device_id}`,
battery_level: heartbeat.battery_level,
signal_strength: heartbeat.signal_strength,
temperature: heartbeat.temperature
}
}))
];
// Sort by timestamp descending
activities.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
res.json({
success: true,
data: activities.slice(0, parseInt(limit))
});
} catch (error) {
console.error('Error fetching dashboard activity:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch dashboard activity',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/dashboard/charts/detections - Get detection chart data
router.get('/charts/detections', async (req, res) => {
try {
const { hours = 24, interval = 'hour' } = req.query;
const timeWindow = new Date(Date.now() - hours * 60 * 60 * 1000);
let groupBy;
switch (interval) {
case 'minute':
groupBy = "DATE_TRUNC('minute', server_timestamp)";
break;
case 'hour':
groupBy = "DATE_TRUNC('hour', server_timestamp)";
break;
case 'day':
groupBy = "DATE_TRUNC('day', server_timestamp)";
break;
default:
groupBy = "DATE_TRUNC('hour', server_timestamp)";
}
const detectionCounts = await DroneDetection.findAll({
where: { server_timestamp: { [Op.gte]: timeWindow } },
attributes: [
[sequelize.fn('DATE_TRUNC', interval, sequelize.col('server_timestamp')), 'time_bucket'],
[sequelize.fn('COUNT', '*'), 'count']
],
group: ['time_bucket'],
order: [['time_bucket', 'ASC']],
raw: true
});
res.json({
success: true,
data: detectionCounts.map(item => ({
timestamp: item.time_bucket,
count: parseInt(item.count)
}))
});
} catch (error) {
console.error('Error fetching detection chart data:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch detection chart data',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/dashboard/charts/devices - Get device activity chart data
router.get('/charts/devices', async (req, res) => {
try {
const { hours = 24 } = req.query;
const timeWindow = new Date(Date.now() - hours * 60 * 60 * 1000);
const deviceActivity = await DroneDetection.findAll({
where: { server_timestamp: { [Op.gte]: timeWindow } },
attributes: [
'device_id',
[sequelize.fn('COUNT', '*'), 'detection_count']
],
include: [{
model: Device,
as: 'device',
attributes: ['name', 'location_description']
}],
group: ['device_id', 'device.id', 'device.name', 'device.location_description'],
order: [[sequelize.fn('COUNT', '*'), 'DESC']],
raw: false
});
res.json({
success: true,
data: deviceActivity.map(item => ({
device_id: item.device_id,
device_name: item.device.name || `Device ${item.device_id}`,
location: item.device.location_description,
detection_count: parseInt(item.dataValues.detection_count)
}))
});
} catch (error) {
console.error('Error fetching device chart data:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch device chart data',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
module.exports = router;

318
server/routes/device.js Normal file
View File

@@ -0,0 +1,318 @@
const express = require('express');
const router = express.Router();
const Joi = require('joi');
const { Device, DroneDetection, Heartbeat } = require('../models');
const { validateRequest } = require('../middleware/validation');
const { authenticateToken } = require('../middleware/auth');
const { Op } = require('sequelize');
// Validation schema for device
const deviceSchema = Joi.object({
id: Joi.number().integer().required(),
name: Joi.string().max(255).optional(),
geo_lat: Joi.number().min(-90).max(90).optional(),
geo_lon: Joi.number().min(-180).max(180).optional(),
location_description: Joi.string().optional(),
heartbeat_interval: Joi.number().integer().min(60).max(3600).optional(),
firmware_version: Joi.string().optional(),
installation_date: Joi.date().optional(),
notes: Joi.string().optional()
});
const updateDeviceSchema = Joi.object({
name: Joi.string().max(255).optional(),
geo_lat: Joi.number().min(-90).max(90).optional(),
geo_lon: Joi.number().min(-180).max(180).optional(),
location_description: Joi.string().optional(),
is_active: Joi.boolean().optional(),
heartbeat_interval: Joi.number().integer().min(60).max(3600).optional(),
firmware_version: Joi.string().optional(),
installation_date: Joi.date().optional(),
notes: Joi.string().optional()
});
// GET /api/devices - Get all devices
router.get('/', async (req, res) => {
try {
const {
include_stats = false,
active_only = false,
limit = 100,
offset = 0
} = req.query;
const whereClause = {};
if (active_only === 'true') {
whereClause.is_active = true;
}
const includeOptions = [];
if (include_stats === 'true') {
// Include latest heartbeat and detection count
includeOptions.push({
model: Heartbeat,
as: 'heartbeats',
limit: 1,
order: [['received_at', 'DESC']],
required: false,
attributes: ['received_at', 'battery_level', 'signal_strength', 'temperature']
});
}
const devices = await Device.findAndCountAll({
where: whereClause,
include: includeOptions,
limit: Math.min(parseInt(limit), 1000),
offset: parseInt(offset),
order: [['created_at', 'DESC']]
});
// If stats requested, get detection counts
let devicesWithStats = devices.rows;
if (include_stats === 'true') {
devicesWithStats = await Promise.all(devices.rows.map(async (device) => {
const detectionCount = await DroneDetection.count({
where: {
device_id: device.id,
server_timestamp: {
[Op.gte]: new Date(Date.now() - 24 * 60 * 60 * 1000) // Last 24 hours
}
}
});
const now = new Date();
const timeSinceLastHeartbeat = device.last_heartbeat
? (now - new Date(device.last_heartbeat)) / 1000
: null;
const expectedInterval = device.heartbeat_interval || 300;
const isOnline = timeSinceLastHeartbeat && timeSinceLastHeartbeat < (expectedInterval * 2);
return {
...device.toJSON(),
stats: {
detections_24h: detectionCount,
status: device.is_active ? (isOnline ? 'online' : 'offline') : 'inactive',
time_since_last_heartbeat: timeSinceLastHeartbeat
}
};
}));
}
res.json({
success: true,
data: devicesWithStats,
pagination: {
total: devices.count,
limit: parseInt(limit),
offset: parseInt(offset),
pages: Math.ceil(devices.count / parseInt(limit))
}
});
} catch (error) {
console.error('Error fetching devices:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch devices',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/devices/map - Get devices with location data for map display
router.get('/map', async (req, res) => {
try {
const devices = await Device.findAll({
where: {
is_active: true,
geo_lat: { [Op.ne]: null },
geo_lon: { [Op.ne]: null }
},
attributes: [
'id',
'name',
'geo_lat',
'geo_lon',
'location_description',
'last_heartbeat'
]
});
// Get recent detections for each device
const devicesWithDetections = await Promise.all(devices.map(async (device) => {
const recentDetections = await DroneDetection.count({
where: {
device_id: device.id,
server_timestamp: {
[Op.gte]: new Date(Date.now() - 10 * 60 * 1000) // Last 10 minutes
}
}
});
const now = new Date();
const timeSinceLastHeartbeat = device.last_heartbeat
? (now - new Date(device.last_heartbeat)) / 1000
: null;
const isOnline = timeSinceLastHeartbeat && timeSinceLastHeartbeat < 600; // 10 minutes
return {
...device.toJSON(),
has_recent_detections: recentDetections > 0,
detection_count_10m: recentDetections,
status: isOnline ? 'online' : 'offline'
};
}));
res.json({
success: true,
data: devicesWithDetections
});
} catch (error) {
console.error('Error fetching devices for map:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch devices for map',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/devices/:id - Get specific device
router.get('/:id', async (req, res) => {
try {
const device = await Device.findByPk(req.params.id, {
include: [
{
model: Heartbeat,
as: 'heartbeats',
limit: 5,
order: [['received_at', 'DESC']]
},
{
model: DroneDetection,
as: 'detections',
limit: 10,
order: [['server_timestamp', 'DESC']]
}
]
});
if (!device) {
return res.status(404).json({
success: false,
message: 'Device not found'
});
}
res.json({
success: true,
data: device
});
} catch (error) {
console.error('Error fetching device:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch device',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// POST /api/devices - Create new device (admin only)
router.post('/', authenticateToken, validateRequest(deviceSchema), async (req, res) => {
try {
const device = await Device.create(req.body);
res.status(201).json({
success: true,
data: device,
message: 'Device created successfully'
});
} catch (error) {
console.error('Error creating device:', error);
if (error.name === 'SequelizeUniqueConstraintError') {
return res.status(409).json({
success: false,
message: 'Device with this ID already exists'
});
}
res.status(500).json({
success: false,
message: 'Failed to create device',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// PUT /api/devices/:id - Update device (admin only)
router.put('/:id', authenticateToken, validateRequest(updateDeviceSchema), async (req, res) => {
try {
const device = await Device.findByPk(req.params.id);
if (!device) {
return res.status(404).json({
success: false,
message: 'Device not found'
});
}
await device.update(req.body);
// Emit real-time update
req.io.emit('device_updated', device);
res.json({
success: true,
data: device,
message: 'Device updated successfully'
});
} catch (error) {
console.error('Error updating device:', error);
res.status(500).json({
success: false,
message: 'Failed to update device',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// DELETE /api/devices/:id - Delete device (admin only)
router.delete('/:id', authenticateToken, async (req, res) => {
try {
const device = await Device.findByPk(req.params.id);
if (!device) {
return res.status(404).json({
success: false,
message: 'Device not found'
});
}
// Soft delete by setting is_active to false
await device.update({ is_active: false });
res.json({
success: true,
message: 'Device deactivated successfully'
});
} catch (error) {
console.error('Error deleting device:', error);
res.status(500).json({
success: false,
message: 'Failed to delete device',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
module.exports = router;

View File

@@ -0,0 +1,228 @@
const express = require('express');
const router = express.Router();
const Joi = require('joi');
const { DroneDetection, Device } = require('../models');
const { processAlert } = require('../services/alertService');
const { validateRequest } = require('../middleware/validation');
// Validation schema for drone detection
const droneDetectionSchema = Joi.object({
device_id: Joi.number().integer().required(),
geo_lat: Joi.number().min(-90).max(90).default(0),
geo_lon: Joi.number().min(-180).max(180).default(0),
device_timestamp: Joi.number().integer().min(0).default(0),
drone_type: Joi.number().integer().min(0).default(0),
rssi: Joi.number().integer().default(0),
freq: Joi.number().integer().required(),
drone_id: Joi.number().integer().required(),
confidence_level: Joi.number().min(0).max(1).optional(),
signal_duration: Joi.number().integer().min(0).optional()
});
// POST /api/detections - Receive drone detection data
router.post('/', validateRequest(droneDetectionSchema), async (req, res) => {
try {
const detectionData = req.body;
// Ensure device exists or create it
const [device] = await Device.findOrCreate({
where: { id: detectionData.device_id },
defaults: {
id: detectionData.device_id,
geo_lat: detectionData.geo_lat || 0,
geo_lon: detectionData.geo_lon || 0,
last_heartbeat: new Date()
}
});
// Create the detection record
const detection = await DroneDetection.create({
...detectionData,
server_timestamp: new Date()
});
// Emit real-time update via Socket.IO
req.io.emit('drone_detection', {
id: detection.id,
device_id: detection.device_id,
drone_id: detection.drone_id,
drone_type: detection.drone_type,
rssi: detection.rssi,
freq: detection.freq,
geo_lat: detection.geo_lat,
geo_lon: detection.geo_lon,
server_timestamp: detection.server_timestamp,
device: {
id: device.id,
name: device.name,
geo_lat: device.geo_lat,
geo_lon: device.geo_lon
}
});
// Process alerts asynchronously
processAlert(detection).catch(error => {
console.error('Alert processing error:', error);
});
res.status(201).json({
success: true,
data: detection,
message: 'Drone detection recorded successfully'
});
} catch (error) {
console.error('Error creating drone detection:', error);
res.status(500).json({
success: false,
message: 'Failed to record drone detection',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/detections - Get drone detections with filtering
router.get('/', async (req, res) => {
try {
const {
device_id,
drone_id,
start_date,
end_date,
limit = 100,
offset = 0,
order = 'DESC'
} = req.query;
const whereClause = {};
if (device_id) whereClause.device_id = device_id;
if (drone_id) whereClause.drone_id = drone_id;
if (start_date || end_date) {
whereClause.server_timestamp = {};
if (start_date) whereClause.server_timestamp[Op.gte] = new Date(start_date);
if (end_date) whereClause.server_timestamp[Op.lte] = new Date(end_date);
}
const detections = await DroneDetection.findAndCountAll({
where: whereClause,
include: [{
model: Device,
as: 'device',
attributes: ['id', 'name', 'geo_lat', 'geo_lon', 'location_description']
}],
limit: Math.min(parseInt(limit), 1000), // Max 1000 records
offset: parseInt(offset),
order: [['server_timestamp', order]]
});
res.json({
success: true,
data: detections.rows,
pagination: {
total: detections.count,
limit: parseInt(limit),
offset: parseInt(offset),
pages: Math.ceil(detections.count / parseInt(limit))
}
});
} catch (error) {
console.error('Error fetching drone detections:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch drone detections',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/detections/stats - Get detection statistics
router.get('/stats', async (req, res) => {
try {
const { device_id, hours = 24 } = req.query;
const whereClause = {
server_timestamp: {
[Op.gte]: new Date(Date.now() - hours * 60 * 60 * 1000)
}
};
if (device_id) whereClause.device_id = device_id;
const [totalDetections, uniqueDrones, uniqueDevices, avgRssi] = await Promise.all([
DroneDetection.count({ where: whereClause }),
DroneDetection.count({
where: whereClause,
distinct: true,
col: 'drone_id'
}),
DroneDetection.count({
where: whereClause,
distinct: true,
col: 'device_id'
}),
DroneDetection.findAll({
where: whereClause,
attributes: [
[sequelize.fn('AVG', sequelize.col('rssi')), 'avg_rssi']
]
})
]);
res.json({
success: true,
data: {
total_detections: totalDetections,
unique_drones: uniqueDrones,
active_devices: uniqueDevices,
average_rssi: Math.round(avgRssi[0]?.dataValues?.avg_rssi || 0),
time_period_hours: hours
}
});
} catch (error) {
console.error('Error fetching detection stats:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch detection statistics',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/detections/:id - Get specific detection
router.get('/:id', async (req, res) => {
try {
const detection = await DroneDetection.findByPk(req.params.id, {
include: [{
model: Device,
as: 'device',
attributes: ['id', 'name', 'geo_lat', 'geo_lon', 'location_description']
}]
});
if (!detection) {
return res.status(404).json({
success: false,
message: 'Detection not found'
});
}
res.json({
success: true,
data: detection
});
} catch (error) {
console.error('Error fetching detection:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch detection',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
module.exports = router;

54
server/routes/health.js Normal file
View File

@@ -0,0 +1,54 @@
const express = require('express');
const router = express.Router();
// Health check endpoint
router.get('/', (req, res) => {
const healthcheck = {
uptime: process.uptime(),
message: 'OK',
timestamp: Date.now(),
environment: process.env.NODE_ENV || 'development',
version: process.env.npm_package_version || '1.0.0'
};
try {
res.status(200).json(healthcheck);
} catch (error) {
healthcheck.message = error;
res.status(503).json(healthcheck);
}
});
// Detailed health check with database connection
router.get('/detailed', async (req, res) => {
const healthcheck = {
uptime: process.uptime(),
message: 'OK',
timestamp: Date.now(),
environment: process.env.NODE_ENV || 'development',
version: process.env.npm_package_version || '1.0.0',
services: {}
};
try {
// Check database connection
const { sequelize } = require('../models');
await sequelize.authenticate();
healthcheck.services.database = 'connected';
// Check Redis connection (if configured)
if (process.env.REDIS_HOST) {
// Add Redis check if implemented
healthcheck.services.redis = 'not_implemented';
}
res.status(200).json(healthcheck);
} catch (error) {
healthcheck.message = 'Service Unavailable';
healthcheck.services.database = 'disconnected';
healthcheck.error = error.message;
res.status(503).json(healthcheck);
}
});
module.exports = router;

199
server/routes/heartbeat.js Normal file
View File

@@ -0,0 +1,199 @@
const express = require('express');
const router = express.Router();
const Joi = require('joi');
const { Heartbeat, Device } = require('../models');
const { validateRequest } = require('../middleware/validation');
// Validation schema for heartbeat
const heartbeatSchema = Joi.object({
type: Joi.string().valid('heartbeat').required(),
key: Joi.string().required(),
device_id: Joi.number().integer().optional(),
signal_strength: Joi.number().integer().optional(),
battery_level: Joi.number().integer().min(0).max(100).optional(),
temperature: Joi.number().optional(),
uptime: Joi.number().integer().min(0).optional(),
memory_usage: Joi.number().integer().min(0).max(100).optional(),
firmware_version: Joi.string().optional()
});
// POST /api/heartbeat - Receive heartbeat from devices
router.post('/', validateRequest(heartbeatSchema), async (req, res) => {
try {
const { type, key, device_id, ...heartbeatData } = req.body;
// If device_id is not provided, try to find device by key
let deviceId = device_id;
if (!deviceId) {
// Try to extract device ID from key or use key as identifier
// This is a fallback for devices that only send key
const keyMatch = key.match(/device[_-]?(\d+)/i);
deviceId = keyMatch ? parseInt(keyMatch[1]) : key.hashCode(); // Simple hash if no pattern
}
// Ensure device exists or create it
const [device] = await Device.findOrCreate({
where: { id: deviceId },
defaults: {
id: deviceId,
name: `Device ${deviceId}`,
last_heartbeat: new Date()
}
});
// Update device's last heartbeat
await device.update({ last_heartbeat: new Date() });
// Create heartbeat record
const heartbeat = await Heartbeat.create({
device_id: deviceId,
device_key: key,
...heartbeatData,
received_at: new Date()
});
// Emit real-time update via Socket.IO
req.io.emit('device_heartbeat', {
device_id: deviceId,
device_key: key,
timestamp: heartbeat.received_at,
status: 'online',
...heartbeatData
});
res.status(201).json({
success: true,
data: heartbeat,
message: 'Heartbeat recorded successfully'
});
} catch (error) {
console.error('Error processing heartbeat:', error);
res.status(500).json({
success: false,
message: 'Failed to process heartbeat',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/heartbeat/status - Get device status overview
router.get('/status', async (req, res) => {
try {
const devices = await Device.findAll({
attributes: [
'id',
'name',
'geo_lat',
'geo_lon',
'last_heartbeat',
'heartbeat_interval',
'is_active'
],
include: [{
model: Heartbeat,
as: 'heartbeats',
limit: 1,
order: [['received_at', 'DESC']],
attributes: ['battery_level', 'signal_strength', 'temperature', 'firmware_version']
}]
});
const now = new Date();
const deviceStatus = devices.map(device => {
const timeSinceLastHeartbeat = device.last_heartbeat
? (now - new Date(device.last_heartbeat)) / 1000
: null;
const expectedInterval = device.heartbeat_interval || 300; // 5 minutes default
const isOnline = timeSinceLastHeartbeat && timeSinceLastHeartbeat < (expectedInterval * 2);
return {
device_id: device.id,
name: device.name,
geo_lat: device.geo_lat,
geo_lon: device.geo_lon,
status: device.is_active ? (isOnline ? 'online' : 'offline') : 'inactive',
last_heartbeat: device.last_heartbeat,
time_since_last_heartbeat: timeSinceLastHeartbeat,
latest_data: device.heartbeats[0] || null
};
});
const summary = {
total_devices: devices.length,
online: deviceStatus.filter(d => d.status === 'online').length,
offline: deviceStatus.filter(d => d.status === 'offline').length,
inactive: deviceStatus.filter(d => d.status === 'inactive').length
};
res.json({
success: true,
data: {
summary,
devices: deviceStatus
}
});
} catch (error) {
console.error('Error fetching device status:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch device status',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/heartbeat/device/:deviceId - Get heartbeat history for specific device
router.get('/device/:deviceId', async (req, res) => {
try {
const { deviceId } = req.params;
const { limit = 50, offset = 0 } = req.query;
const heartbeats = await Heartbeat.findAndCountAll({
where: { device_id: deviceId },
limit: Math.min(parseInt(limit), 1000),
offset: parseInt(offset),
order: [['received_at', 'DESC']],
include: [{
model: Device,
as: 'device',
attributes: ['id', 'name', 'geo_lat', 'geo_lon']
}]
});
res.json({
success: true,
data: heartbeats.rows,
pagination: {
total: heartbeats.count,
limit: parseInt(limit),
offset: parseInt(offset),
pages: Math.ceil(heartbeats.count / parseInt(limit))
}
});
} catch (error) {
console.error('Error fetching device heartbeats:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch device heartbeats',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// Helper function to generate simple hash from string
String.prototype.hashCode = function() {
let hash = 0;
if (this.length === 0) return hash;
for (let i = 0; i < this.length; i++) {
const char = this.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return Math.abs(hash);
};
module.exports = router;

45
server/routes/index.js Normal file
View File

@@ -0,0 +1,45 @@
const express = require('express');
const router = express.Router();
// Import route modules
const droneDetectionRoutes = require('./droneDetection');
const heartbeatRoutes = require('./heartbeat');
const deviceRoutes = require('./device');
const userRoutes = require('./user');
const alertRoutes = require('./alert');
const dashboardRoutes = require('./dashboard');
// API versioning
router.use('/v1/detections', droneDetectionRoutes);
router.use('/v1/heartbeat', heartbeatRoutes);
router.use('/v1/devices', deviceRoutes);
router.use('/v1/users', userRoutes);
router.use('/v1/alerts', alertRoutes);
router.use('/v1/dashboard', dashboardRoutes);
// Default routes (no version prefix for backward compatibility)
router.use('/detections', droneDetectionRoutes);
router.use('/heartbeat', heartbeatRoutes);
router.use('/devices', deviceRoutes);
router.use('/users', userRoutes);
router.use('/alerts', alertRoutes);
router.use('/dashboard', dashboardRoutes);
// API documentation endpoint
router.get('/', (req, res) => {
res.json({
message: 'Drone Detection System API',
version: '1.0.0',
endpoints: {
detections: '/api/detections',
heartbeat: '/api/heartbeat',
devices: '/api/devices',
users: '/api/users',
alerts: '/api/alerts',
dashboard: '/api/dashboard'
},
documentation: '/api/docs'
});
});
module.exports = router;

214
server/routes/user.js Normal file
View File

@@ -0,0 +1,214 @@
const express = require('express');
const router = express.Router();
const Joi = require('joi');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { User } = require('../models');
const { validateRequest } = require('../middleware/validation');
const { authenticateToken, requireRole } = require('../middleware/auth');
// Validation schemas
const registerSchema = Joi.object({
username: Joi.string().min(3).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
first_name: Joi.string().optional(),
last_name: Joi.string().optional(),
phone_number: Joi.string().optional(),
role: Joi.string().valid('admin', 'operator', 'viewer').default('viewer')
});
const loginSchema = Joi.object({
username: Joi.string().required(),
password: Joi.string().required()
});
const updateProfileSchema = Joi.object({
first_name: Joi.string().optional(),
last_name: Joi.string().optional(),
phone_number: Joi.string().optional(),
sms_alerts_enabled: Joi.boolean().optional(),
email_alerts_enabled: Joi.boolean().optional(),
timezone: Joi.string().optional()
});
// POST /api/users/register - Register new user
router.post('/register', validateRequest(registerSchema), async (req, res) => {
try {
const { password, ...userData } = req.body;
// Hash password
const saltRounds = 12;
const password_hash = await bcrypt.hash(password, saltRounds);
// Create user
const user = await User.create({
...userData,
password_hash
});
// Remove password hash from response
const { password_hash: _, ...userResponse } = user.toJSON();
res.status(201).json({
success: true,
data: userResponse,
message: 'User registered successfully'
});
} catch (error) {
console.error('Error registering user:', error);
if (error.name === 'SequelizeUniqueConstraintError') {
return res.status(409).json({
success: false,
message: 'Username or email already exists'
});
}
res.status(500).json({
success: false,
message: 'Failed to register user',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// POST /api/users/login - User login
router.post('/login', validateRequest(loginSchema), async (req, res) => {
try {
const { username, password } = req.body;
// Find user by username or email
const user = await User.findOne({
where: {
[Op.or]: [
{ username: username },
{ email: username }
],
is_active: true
}
});
if (!user || !await bcrypt.compare(password, user.password_hash)) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Update last login
await user.update({ last_login: new Date() });
// Generate JWT token
const token = jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
// Remove password hash from response
const { password_hash: _, ...userResponse } = user.toJSON();
res.json({
success: true,
data: {
user: userResponse,
token,
expires_in: '24h'
},
message: 'Login successful'
});
} catch (error) {
console.error('Error during login:', error);
res.status(500).json({
success: false,
message: 'Login failed',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/users/profile - Get current user profile
router.get('/profile', authenticateToken, async (req, res) => {
try {
const { password_hash: _, ...userProfile } = req.user.toJSON();
res.json({
success: true,
data: userProfile
});
} catch (error) {
console.error('Error fetching user profile:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch user profile',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// PUT /api/users/profile - Update user profile
router.put('/profile', authenticateToken, validateRequest(updateProfileSchema), async (req, res) => {
try {
await req.user.update(req.body);
const { password_hash: _, ...userResponse } = req.user.toJSON();
res.json({
success: true,
data: userResponse,
message: 'Profile updated successfully'
});
} catch (error) {
console.error('Error updating user profile:', error);
res.status(500).json({
success: false,
message: 'Failed to update profile',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
// GET /api/users - Get all users (admin only)
router.get('/', authenticateToken, requireRole(['admin']), async (req, res) => {
try {
const { limit = 50, offset = 0, role, is_active } = req.query;
const whereClause = {};
if (role) whereClause.role = role;
if (is_active !== undefined) whereClause.is_active = is_active === 'true';
const users = await User.findAndCountAll({
where: whereClause,
attributes: { exclude: ['password_hash'] },
limit: Math.min(parseInt(limit), 100),
offset: parseInt(offset),
order: [['created_at', 'DESC']]
});
res.json({
success: true,
data: users.rows,
pagination: {
total: users.count,
limit: parseInt(limit),
offset: parseInt(offset),
pages: Math.ceil(users.count / parseInt(limit))
}
});
} catch (error) {
console.error('Error fetching users:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch users',
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
});
}
});
module.exports = router;

View File

@@ -0,0 +1,17 @@
-- Database initialization script for Docker
-- This script sets up the initial database structure
-- Enable extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Set timezone
SET timezone = 'Europe/Stockholm';
-- Create indexes for better performance
-- (These will be created by Sequelize, but we can optimize here)
-- Log successful initialization
INSERT INTO pg_stat_statements_info (dealloc) VALUES (0) ON CONFLICT DO NOTHING;
-- Grant necessary permissions
GRANT ALL PRIVILEGES ON DATABASE drone_detection TO postgres;

View File

@@ -0,0 +1,331 @@
const { Sequelize } = require('sequelize');
const bcrypt = require('bcrypt');
// Import models
const Device = require('../models/Device');
const DroneDetection = require('../models/DroneDetection');
const Heartbeat = require('../models/Heartbeat');
const User = require('../models/User');
const AlertRule = require('../models/AlertRule');
const AlertLog = require('../models/AlertLog');
const setupDatabase = async () => {
try {
console.log('🚀 Starting database setup...\n');
// Load environment variables
require('dotenv').config();
// Create Sequelize instance
const sequelize = new Sequelize(
process.env.DB_NAME,
process.env.DB_USER,
process.env.DB_PASSWORD,
{
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: 'postgres',
logging: console.log,
}
);
// Test database connection
console.log('📡 Testing database connection...');
await sequelize.authenticate();
console.log('✅ Database connection established successfully.\n');
// Initialize models
console.log('📋 Initializing models...');
Device.init(Device.getAttributes(), { sequelize, modelName: 'Device' });
DroneDetection.init(DroneDetection.getAttributes(), { sequelize, modelName: 'DroneDetection' });
Heartbeat.init(Heartbeat.getAttributes(), { sequelize, modelName: 'Heartbeat' });
User.init(User.getAttributes(), { sequelize, modelName: 'User' });
AlertRule.init(AlertRule.getAttributes(), { sequelize, modelName: 'AlertRule' });
AlertLog.init(AlertLog.getAttributes(), { sequelize, modelName: 'AlertLog' });
// Define associations
console.log('🔗 Setting up model associations...');
// Device associations
Device.hasMany(DroneDetection, { foreignKey: 'device_id', as: 'detections' });
Device.hasMany(Heartbeat, { foreignKey: 'device_id', as: 'heartbeats' });
// Detection associations
DroneDetection.belongsTo(Device, { foreignKey: 'device_id', as: 'device' });
// Heartbeat associations
Heartbeat.belongsTo(Device, { foreignKey: 'device_id', as: 'device' });
// User associations
User.hasMany(AlertRule, { foreignKey: 'user_id', as: 'alertRules' });
User.hasMany(AlertLog, { foreignKey: 'user_id', as: 'alertLogs' });
// Alert associations
AlertRule.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
AlertRule.hasMany(AlertLog, { foreignKey: 'alert_rule_id', as: 'logs' });
AlertLog.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
AlertLog.belongsTo(AlertRule, { foreignKey: 'alert_rule_id', as: 'alertRule' });
AlertLog.belongsTo(DroneDetection, { foreignKey: 'detection_id', as: 'detection' });
// Sync database (create tables)
console.log('🏗️ Creating database tables...');
await sequelize.sync({ force: true }); // WARNING: This will drop existing tables
console.log('✅ Database tables created successfully.\n');
// Create sample data
console.log('📊 Creating sample data...\n');
// Create admin user
console.log('👤 Creating admin user...');
const adminUser = await User.create({
username: 'admin',
email: 'admin@example.com',
password: await bcrypt.hash('admin123', 10),
role: 'admin'
});
console.log(`✅ Admin user created: ${adminUser.username}`);
// Create operator user
console.log('👤 Creating operator user...');
const operatorUser = await User.create({
username: 'operator',
email: 'operator@example.com',
password: await bcrypt.hash('operator123', 10),
role: 'operator'
});
console.log(`✅ Operator user created: ${operatorUser.username}`);
// Create sample devices
console.log('📡 Creating sample devices...');
const devices = await Device.bulkCreate([
{
device_id: 1941875381,
name: 'Drone Detector Alpha',
location: 'Stockholm Central',
geo_lat: 59.3293,
geo_lon: 18.0686,
status: 'online',
last_seen: new Date()
},
{
device_id: 1941875382,
name: 'Drone Detector Beta',
location: 'Gothenburg Port',
geo_lat: 57.7089,
geo_lon: 11.9746,
status: 'online',
last_seen: new Date()
},
{
device_id: 1941875383,
name: 'Drone Detector Gamma',
location: 'Malmö Airport',
geo_lat: 55.6050,
geo_lon: 13.0038,
status: 'offline',
last_seen: new Date(Date.now() - 2 * 60 * 60 * 1000) // 2 hours ago
}
]);
console.log(`✅ Created ${devices.length} sample devices`);
// Create sample heartbeats
console.log('💓 Creating sample heartbeats...');
const heartbeats = await Heartbeat.bulkCreate([
{
device_id: 1941875381,
battery_level: 85,
signal_strength: -45,
temperature: 22.5,
status: 'active',
timestamp: new Date()
},
{
device_id: 1941875382,
battery_level: 72,
signal_strength: -38,
temperature: 24.1,
status: 'active',
timestamp: new Date()
}
]);
console.log(`✅ Created ${heartbeats.length} sample heartbeats`);
// Create sample drone detections
console.log('🚁 Creating sample drone detections...');
const detections = await DroneDetection.bulkCreate([
{
device_id: 1941875381,
geo_lat: 59.3293,
geo_lon: 18.0686,
device_timestamp: Math.floor(Date.now() / 1000),
drone_type: 0,
rssi: -45,
freq: 20,
drone_id: 1001,
timestamp: new Date(),
threat_level: 'high',
estimated_distance: 150,
requires_action: true
},
{
device_id: 1941875382,
geo_lat: 57.7089,
geo_lon: 11.9746,
device_timestamp: Math.floor(Date.now() / 1000) - 3600,
drone_type: 1,
rssi: -52,
freq: 25,
drone_id: 1002,
timestamp: new Date(Date.now() - 60 * 60 * 1000),
threat_level: 'medium',
estimated_distance: 800,
requires_action: false
},
{
device_id: 1941875381,
geo_lat: 59.3295,
geo_lon: 18.0690,
device_timestamp: Math.floor(Date.now() / 1000) - 7200,
drone_type: 0,
rssi: -75,
freq: 22,
drone_id: 1003,
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
threat_level: 'low',
estimated_distance: 2500,
requires_action: false
}
]);
console.log(`✅ Created ${detections.length} sample drone detections`);
// Create sample alert rules
console.log('🚨 Creating sample alert rules...');
const alertRules = await AlertRule.bulkCreate([
{
user_id: adminUser.id,
name: 'Critical Security Threat',
description: 'Immediate alert for critical and high threats to government facilities',
conditions: {
min_threat_level: 'high',
rssi_threshold: -55,
max_distance: 200,
drone_types: [0, 1, 2],
device_ids: []
},
actions: {
sms: true,
phone_number: '+46701234567',
email: true,
channels: ['sms', 'email']
},
cooldown_minutes: 2,
is_active: true
},
{
user_id: operatorUser.id,
name: 'Medium Threat Monitoring',
description: 'Monitor medium threat drones in facility vicinity',
conditions: {
min_threat_level: 'medium',
rssi_threshold: -70,
max_distance: 1000,
drone_types: [1, 2],
device_ids: [1941875381, 1941875382]
},
actions: {
sms: true,
phone_number: '+46709876543',
channels: ['sms']
},
cooldown_minutes: 10,
is_active: true
},
{
user_id: adminUser.id,
name: 'Device Offline Alert',
description: 'Alert when security devices go offline',
conditions: {
device_offline: true,
device_ids: [1941875381, 1941875382, 1941875383]
},
actions: {
sms: true,
phone_number: '+46701234567',
channels: ['sms']
},
cooldown_minutes: 30,
is_active: true
}
]);
console.log(`✅ Created ${alertRules.length} sample alert rules`);
// Create sample alert logs
console.log('📝 Creating sample alert logs...');
const alertLogs = await AlertLog.bulkCreate([
{
user_id: adminUser.id,
alert_rule_id: alertRules[0].id,
detection_id: detections[0].id,
message: 'Drone detected with strong signal',
status: 'sent',
sent_at: new Date()
},
{
user_id: operatorUser.id,
alert_rule_id: alertRules[1].id,
detection_id: null,
message: 'Device 1941875383 went offline',
status: 'sent',
sent_at: new Date(Date.now() - 30 * 60 * 1000)
}
]);
console.log(`✅ Created ${alertLogs.length} sample alert logs\n`);
// Create database indexes for performance
console.log('🚀 Creating database indexes...');
await sequelize.query(`
CREATE INDEX IF NOT EXISTS idx_drone_detections_device_timestamp
ON "DroneDetections" (device_id, timestamp);
`);
await sequelize.query(`
CREATE INDEX IF NOT EXISTS idx_heartbeats_device_timestamp
ON "Heartbeats" (device_id, timestamp);
`);
await sequelize.query(`
CREATE INDEX IF NOT EXISTS idx_alert_logs_user_created
ON "AlertLogs" (user_id, "createdAt");
`);
console.log('✅ Database indexes created\n');
console.log('🎉 Database setup completed successfully!\n');
console.log('📋 Summary:');
console.log(` • Users created: 2 (admin, operator)`);
console.log(` • Devices created: ${devices.length}`);
console.log(` • Heartbeats created: ${heartbeats.length}`);
console.log(` • Detections created: ${detections.length}`);
console.log(` • Alert rules created: ${alertRules.length}`);
console.log(` • Alert logs created: ${alertLogs.length}`);
console.log('\n📝 Default login credentials:');
console.log(' Admin: admin / admin123');
console.log(' Operator: operator / operator123\n');
// Close connection
await sequelize.close();
} catch (error) {
console.error('❌ Database setup failed:', error);
process.exit(1);
}
};
// Run setup if called directly
if (require.main === module) {
setupDatabase();
}
module.exports = setupDatabase;

View File

@@ -0,0 +1,499 @@
const twilio = require('twilio');
const { AlertRule, AlertLog, User, Device } = require('../models');
const { Op } = require('sequelize');
class AlertService {
constructor() {
this.twilioClient = null;
this.initializeTwilio();
}
// RSSI-based threat assessment for security installations
assessThreatLevel(rssi, droneType) {
// RSSI typically ranges from -30 (very close) to -100 (very far)
// For 15km range detection, we need to establish threat zones for sensitive facilities
let threatLevel = 'low';
let estimatedDistance = 0;
let description = '';
let actionRequired = false;
// Convert RSSI to estimated distance (rough calculation)
// Formula: Distance (m) = 10^((RSSI_at_1m - RSSI) / (10 * n))
// Where n = path loss exponent (typically 2-4 for outdoor environments)
const rssiAt1m = -30; // Typical RSSI at 1 meter
const pathLossExponent = 3; // Outdoor environment with obstacles
estimatedDistance = Math.pow(10, (rssiAt1m - rssi) / (10 * pathLossExponent));
// Threat level assessment based on distance zones for sensitive facilities
if (rssi >= -40) {
// Very close: 0-50 meters - CRITICAL THREAT
threatLevel = 'critical';
description = 'IMMEDIATE THREAT: Drone within security perimeter (0-50m)';
actionRequired = true;
} else if (rssi >= -55) {
// Close: 50-200 meters - HIGH THREAT
threatLevel = 'high';
description = 'HIGH THREAT: Drone approaching facility (50-200m)';
actionRequired = true;
} else if (rssi >= -70) {
// Medium: 200-1000 meters - MEDIUM THREAT
threatLevel = 'medium';
description = 'MEDIUM THREAT: Drone in facility vicinity (200m-1km)';
actionRequired = false;
} else if (rssi >= -85) {
// Far: 1-5 kilometers - LOW THREAT
threatLevel = 'low';
description = 'LOW THREAT: Drone detected at distance (1-5km)';
actionRequired = false;
} else {
// Very far: 5-15 kilometers - MONITORING ONLY
threatLevel = 'monitoring';
description = 'MONITORING: Drone detected at long range (5-15km)';
actionRequired = false;
}
// Adjust threat level based on drone type (if classified)
const droneTypes = {
0: 'Consumer/Hobby',
1: 'Professional/Military',
2: 'Racing/High-speed',
3: 'Unknown/Custom'
};
if (droneType === 1) {
// Military/Professional drone - escalate threat
if (threatLevel === 'low') threatLevel = 'medium';
if (threatLevel === 'medium') threatLevel = 'high';
if (threatLevel === 'high') threatLevel = 'critical';
description += ' - PROFESSIONAL/MILITARY DRONE DETECTED';
actionRequired = true;
} else if (droneType === 2) {
// Racing/Fast drone - escalate if close
if (rssi >= -55 && threatLevel !== 'critical') {
threatLevel = 'high';
description += ' - HIGH-SPEED DRONE DETECTED';
actionRequired = true;
}
}
return {
level: threatLevel,
estimatedDistance: Math.round(estimatedDistance),
rssi,
droneType: droneTypes[droneType] || 'Unknown',
description,
requiresImmediateAction: actionRequired,
priority: threatLevel === 'critical' ? 1 : threatLevel === 'high' ? 2 : threatLevel === 'medium' ? 3 : 4
};
}
initializeTwilio() {
if (process.env.TWILIO_ACCOUNT_SID && process.env.TWILIO_AUTH_TOKEN) {
this.twilioClient = twilio(
process.env.TWILIO_ACCOUNT_SID,
process.env.TWILIO_AUTH_TOKEN
);
} else {
console.warn('Twilio credentials not configured. SMS alerts will be disabled.');
}
}
async processAlert(detection) {
try {
console.log(`🔍 Processing alert for detection ${detection.id}`);
// Assess threat level based on RSSI and drone type
const threatAssessment = this.assessThreatLevel(detection.rssi, detection.drone_type);
console.log('⚠️ Threat assessment:', threatAssessment);
// Update detection with threat assessment
await detection.update({
processed: true,
threat_level: threatAssessment.level,
estimated_distance: threatAssessment.estimatedDistance
});
// Get all active alert rules
const alertRules = await AlertRule.findAll({
where: { is_active: true },
include: [{
model: User,
as: 'user',
where: { is_active: true }
}]
});
for (const rule of alertRules) {
if (await this.shouldTriggerAlert(rule, detection, threatAssessment)) {
await this.triggerAlert(rule, detection, threatAssessment);
}
}
} catch (error) {
console.error('Error processing alert:', error);
throw error;
}
}
async shouldTriggerAlert(rule, detection, threatAssessment) {
try {
// SECURITY ENHANCEMENT: Check threat level requirements
if (rule.conditions.min_threat_level) {
const threatLevels = { 'monitoring': 0, 'low': 1, 'medium': 2, 'high': 3, 'critical': 4 };
const requiredLevel = threatLevels[rule.conditions.min_threat_level] || 0;
const currentLevel = threatLevels[threatAssessment.level] || 0;
if (currentLevel < requiredLevel) {
console.log(`Alert rule ${rule.name}: Threat level ${threatAssessment.level} below minimum ${rule.conditions.min_threat_level}`);
return false;
}
}
// SECURITY ENHANCEMENT: For government/sensitive sites, always alert on critical threats
if (threatAssessment.level === 'critical') {
console.log(`🚨 CRITICAL THREAT DETECTED - Force triggering alert for rule ${rule.name}`);
return true;
}
// Check device filter
if (rule.conditions.device_ids && rule.conditions.device_ids.length > 0 &&
!rule.conditions.device_ids.includes(detection.device_id)) {
return false;
}
// Check drone type filter
if (rule.conditions.drone_types && rule.conditions.drone_types.length > 0 &&
!rule.conditions.drone_types.includes(detection.drone_type)) {
return false;
}
// Check RSSI thresholds (enhanced for security)
if (rule.conditions.rssi_threshold && detection.rssi < rule.conditions.rssi_threshold) {
return false;
}
// SECURITY ENHANCEMENT: Check estimated distance
if (rule.conditions.max_distance && threatAssessment.estimatedDistance > rule.conditions.max_distance) {
console.log(`Alert rule ${rule.name}: Distance ${threatAssessment.estimatedDistance}m exceeds maximum ${rule.conditions.max_distance}m`);
return false;
}
// Check frequency ranges
if (rule.frequency_ranges && rule.frequency_ranges.length > 0) {
const inRange = rule.frequency_ranges.some(range =>
detection.freq >= range.min && detection.freq <= range.max
);
if (!inRange) {
return false;
}
}
// Check time window and minimum detections
if (rule.min_detections > 1) {
const timeWindowStart = new Date(Date.now() - rule.time_window * 1000);
const recentDetections = await DroneDetection.count({
where: {
device_id: detection.device_id,
drone_id: detection.drone_id,
server_timestamp: {
[Op.gte]: timeWindowStart
}
}
});
if (recentDetections < rule.min_detections) {
return false;
}
}
// Check cooldown period
if (rule.cooldown_period > 0) {
const cooldownStart = new Date(Date.now() - rule.cooldown_period * 1000);
const recentAlert = await AlertLog.findOne({
where: {
alert_rule_id: rule.id,
status: 'sent',
sent_at: {
[Op.gte]: cooldownStart
}
},
include: [{
model: DroneDetection,
as: 'detection',
where: {
device_id: detection.device_id,
drone_id: detection.drone_id
}
}]
});
if (recentAlert) {
return false;
}
}
// Check active hours and days
if (rule.active_hours || rule.active_days) {
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const currentTime = currentHour * 60 + currentMinute;
const currentDay = now.getDay() || 7; // Convert Sunday from 0 to 7
if (rule.active_days && !rule.active_days.includes(currentDay)) {
return false;
}
if (rule.active_hours) {
const startTime = this.parseTime(rule.active_hours.start);
const endTime = this.parseTime(rule.active_hours.end);
if (startTime !== null && endTime !== null) {
if (startTime <= endTime) {
// Same day range
if (currentTime < startTime || currentTime > endTime) {
return false;
}
} else {
// Overnight range
if (currentTime < startTime && currentTime > endTime) {
return false;
}
}
}
}
}
return true;
} catch (error) {
console.error('Error checking alert conditions:', error);
return false;
}
}
async triggerAlert(rule, detection, threatAssessment) {
try {
const user = rule.user;
const device = await Device.findByPk(detection.device_id);
// Generate enhanced alert message with threat assessment
const message = this.generateSecurityAlertMessage(detection, device, rule, threatAssessment);
// SECURITY ENHANCEMENT: For critical threats, send to all available channels
const channels = threatAssessment.level === 'critical'
? ['sms', 'email', 'webhook'] // Force all channels for critical threats
: rule.actions.channels || ['sms'];
// Send alerts through configured channels
for (const channel of channels) {
let alertLog = null;
try {
switch (channel) {
case 'sms':
if (rule.actions.sms && rule.actions.phone_number) {
alertLog = await this.sendSMSAlert(rule.actions.phone_number, message, rule, detection, threatAssessment);
}
break;
case 'email':
if (rule.actions.email && user.email) {
alertLog = await this.sendEmailAlert(user.email, message, rule, detection, threatAssessment);
}
break;
case 'webhook':
if (rule.actions.webhook_url) {
alertLog = await this.sendWebhookAlert(rule.actions.webhook_url, detection, device, rule, threatAssessment);
}
break;
default:
console.warn(`Unknown alert channel: ${channel}`);
}
if (alertLog) {
console.log(`🚨 ${threatAssessment.level.toUpperCase()} THREAT: Alert sent via ${channel} for detection ${detection.id}`);
}
} catch (channelError) {
console.error(`Error sending ${channel} alert:`, channelError);
// Log failed alert
await AlertLog.create({
alert_rule_id: rule.id,
detection_id: detection.id,
alert_type: channel,
recipient: channel === 'sms' ? user.phone_number :
channel === 'email' ? user.email : rule.webhook_url,
message: message,
status: 'failed',
error_message: channelError.message,
priority: rule.priority
});
}
}
} catch (error) {
console.error('Error triggering alert:', error);
throw error;
}
}
async sendSMSAlert(phoneNumber, message, rule, detection) {
if (!this.twilioClient) {
throw new Error('Twilio not configured');
}
const twilioMessage = await this.twilioClient.messages.create({
body: message,
from: process.env.TWILIO_PHONE_NUMBER,
to: phoneNumber
});
return await AlertLog.create({
alert_rule_id: rule.id,
detection_id: detection.id,
alert_type: 'sms',
recipient: phoneNumber,
message: message,
status: 'sent',
sent_at: new Date(),
external_id: twilioMessage.sid,
priority: rule.priority
});
}
async sendEmailAlert(email, message, rule, detection) {
// Email implementation would go here
// For now, just log the alert
console.log(`Email alert would be sent to ${email}: ${message}`);
return await AlertLog.create({
alert_rule_id: rule.id,
detection_id: detection.id,
alert_type: 'email',
recipient: email,
message: message,
status: 'sent',
sent_at: new Date(),
priority: rule.priority
});
}
async sendWebhookAlert(webhookUrl, detection, device, rule) {
const payload = {
event: 'drone_detection',
timestamp: new Date().toISOString(),
detection: {
id: detection.id,
device_id: detection.device_id,
drone_id: detection.drone_id,
drone_type: detection.drone_type,
rssi: detection.rssi,
freq: detection.freq,
geo_lat: detection.geo_lat,
geo_lon: detection.geo_lon,
server_timestamp: detection.server_timestamp
},
device: {
id: device.id,
name: device.name,
geo_lat: device.geo_lat,
geo_lon: device.geo_lon,
location_description: device.location_description
},
alert_rule: {
id: rule.id,
name: rule.name,
priority: rule.priority
}
};
const response = await fetch(webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'DroneDetectionSystem/1.0'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`Webhook failed with status ${response.status}`);
}
return await AlertLog.create({
alert_rule_id: rule.id,
detection_id: detection.id,
alert_type: 'webhook',
recipient: webhookUrl,
message: JSON.stringify(payload),
status: 'sent',
sent_at: new Date(),
priority: rule.priority
});
}
generateAlertMessage(detection, device, rule) {
const deviceName = device.name || `Device ${device.id}`;
const location = device.location_description ||
`${device.geo_lat}, ${device.geo_lon}` ||
'Unknown location';
return `🚨 DRONE ALERT: Drone detected by ${deviceName} at ${location}. ` +
`Drone ID: ${detection.drone_id}, Frequency: ${detection.freq}MHz, ` +
`RSSI: ${detection.rssi}dBm. Time: ${new Date().toLocaleString()}`;
}
// SECURITY ENHANCEMENT: Enhanced message generation with threat assessment
generateSecurityAlertMessage(detection, device, rule, threatAssessment) {
const timestamp = new Date().toLocaleString('sv-SE', { timeZone: 'Europe/Stockholm' });
const deviceName = device ? device.name : `Device ${detection.device_id}`;
const location = device ? device.location : 'Unknown location';
// Create security-focused alert message
let message = `🚨 SECURITY ALERT 🚨\n`;
message += `THREAT LEVEL: ${threatAssessment.level.toUpperCase()}\n`;
message += `${threatAssessment.description}\n\n`;
message += `📍 LOCATION: ${location}\n`;
message += `🔧 DEVICE: ${deviceName}\n`;
message += `📏 DISTANCE: ~${threatAssessment.estimatedDistance}m\n`;
message += `📶 SIGNAL: ${detection.rssi} dBm\n`;
message += `🚁 DRONE TYPE: ${threatAssessment.droneType}\n`;
message += `⏰ TIME: ${timestamp}\n`;
if (threatAssessment.requiresImmediateAction) {
message += `\n⚠️ IMMEDIATE ACTION REQUIRED\n`;
message += `Security personnel should respond immediately.`;
}
return message;
}
parseTime(timeString) {
if (!timeString) return null;
const match = timeString.match(/^(\d{1,2}):(\d{2})$/);
if (!match) return null;
const hours = parseInt(match[1]);
const minutes = parseInt(match[2]);
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
return null;
}
return hours * 60 + minutes;
}
}
// Export singleton instance
const alertService = new AlertService();
module.exports = {
processAlert: (detection) => alertService.processAlert(detection),
AlertService
};

View File

@@ -0,0 +1,55 @@
function initializeSocketHandlers(io) {
io.on('connection', (socket) => {
console.log(`Client connected: ${socket.id}`);
// Join device-specific rooms for targeted updates
socket.on('join_device_room', (deviceId) => {
socket.join(`device_${deviceId}`);
console.log(`Client ${socket.id} joined device room: device_${deviceId}`);
});
// Join dashboard room for general updates
socket.on('join_dashboard', () => {
socket.join('dashboard');
console.log(`Client ${socket.id} joined dashboard room`);
});
// Leave rooms
socket.on('leave_device_room', (deviceId) => {
socket.leave(`device_${deviceId}`);
console.log(`Client ${socket.id} left device room: device_${deviceId}`);
});
socket.on('leave_dashboard', () => {
socket.leave('dashboard');
console.log(`Client ${socket.id} left dashboard room`);
});
// Handle client disconnect
socket.on('disconnect', () => {
console.log(`Client disconnected: ${socket.id}`);
});
// Send current status on connect
socket.emit('connection_status', {
status: 'connected',
timestamp: new Date().toISOString(),
clientId: socket.id
});
});
// Helper functions to emit events to specific rooms
io.emitToDevice = function(deviceId, event, data) {
io.to(`device_${deviceId}`).emit(event, data);
};
io.emitToDashboard = function(event, data) {
io.to('dashboard').emit(event, data);
};
console.log('Socket.IO handlers initialized');
}
module.exports = {
initializeSocketHandlers
};