Remove legacy backend files and update microservices config

- Delete ARCHITECTURE.md and old services directory - Add sensor-service
and data-ingestion-service to Docker Compose - Comment out unused
services in docker-compose.yml - Update deploy.sh to use `docker
compose` command - Extend API gateway to proxy sensor-service routes and
WebSocket - Refactor health checks and service dependencies
This commit is contained in:
rafaeldpsilva
2025-09-10 14:42:49 +01:00
parent 90c95d6801
commit d4f280de93
6 changed files with 248 additions and 447 deletions

View File

@@ -1,139 +0,0 @@
# Backend Architecture Restructuring
## Overview
The backend has been restructured from a monolithic approach to a clean **3-layer architecture** with proper separation of concerns.
## Architecture Layers
### 1. Infrastructure Layer (`layers/infrastructure/`)
**Responsibility**: Data access, external services, and low-level operations
- **`database_connection.py`** - MongoDB connection management and indexing
- **`redis_connection.py`** - Redis connection and basic operations
- **`repositories.py`** - Data access layer with repository pattern
**Key Principles**:
- No business logic
- Only handles data persistence and retrieval
- Provides abstractions for external services
### 2. Business Layer (`layers/business/`)
**Responsibility**: Business logic, data processing, and core application rules
- **`sensor_service.py`** - Sensor data processing and validation
- **`room_service.py`** - Room metrics calculation and aggregation
- **`analytics_service.py`** - Analytics calculations and reporting
- **`cleanup_service.py`** - Data retention and maintenance
**Key Principles**:
- Contains all business rules and validation
- Independent of presentation concerns
- Uses infrastructure layer for data access
### 3. Presentation Layer (`layers/presentation/`)
**Responsibility**: HTTP endpoints, WebSocket handling, and user interface
- **`api_routes.py`** - REST API endpoints and request/response handling
- **`websocket_handler.py`** - WebSocket connection management
- **`redis_subscriber.py`** - Real-time data broadcasting
**Key Principles**:
- Handles HTTP requests and responses
- Manages real-time communications
- Delegates business logic to business layer
## File Comparison
### Before (Monolithic)
```
main.py (203 lines) # Mixed concerns
api.py (506 lines) # API + some business logic
database.py (220 lines) # DB + Redis + cleanup
persistence.py (448 lines) # Business + data access
models.py (236 lines) # Data models
```
### After (Layered)
```
Infrastructure Layer:
├── database_connection.py (114 lines) # Pure DB connection
├── redis_connection.py (89 lines) # Pure Redis connection
└── repositories.py (376 lines) # Clean data access
Business Layer:
├── sensor_service.py (380 lines) # Sensor business logic
├── room_service.py (242 lines) # Room business logic
├── analytics_service.py (333 lines) # Analytics business logic
└── cleanup_service.py (278 lines) # Cleanup business logic
Presentation Layer:
├── api_routes.py (430 lines) # Pure API endpoints
├── websocket_handler.py (103 lines) # WebSocket management
└── redis_subscriber.py (148 lines) # Real-time broadcasting
Core:
├── main_layered.py (272 lines) # Clean application entry
└── models.py (236 lines) # Unchanged data models
```
## Key Improvements
### 1. **Separation of Concerns**
- Each layer has a single, well-defined responsibility
- Infrastructure concerns isolated from business logic
- Business logic separated from presentation
### 2. **Testability**
- Each layer can be tested independently
- Business logic testable without database dependencies
- Infrastructure layer testable without business complexity
### 3. **Maintainability**
- Changes in one layer don't affect others
- Clear boundaries make code easier to understand
- Reduced coupling between components
### 4. **Scalability**
- Layers can be scaled independently
- Easy to replace implementations within layers
- Clear extension points for new features
### 5. **Dependency Management**
- Clear dependency flow: Presentation → Business → Infrastructure
- No circular dependencies
- Infrastructure layer has no knowledge of business rules
## Usage
### Running the Layered Application
```bash
# Use the new layered main file
conda activate dashboard
uvicorn main_layered:app --reload
```
### Testing the Structure
```bash
# Validate the architecture
python test_structure.py
```
## Benefits Achieved
**Clear separation of concerns**
**Infrastructure isolated from business logic**
**Business logic separated from presentation**
**Easy to test individual layers**
**Maintainable and scalable structure**
**No layering violations detected**
**2,290+ lines properly organized across 10+ files**
## Migration Path
The original files are preserved, so you can:
1. Test the new layered architecture with `main_layered.py`
2. Gradually migrate consumers to use the new structure
3. Remove old files once confident in the new architecture
Both architectures can coexist during the transition period.

View File

@@ -7,7 +7,7 @@ Port: 8000
import asyncio import asyncio
import aiohttp import aiohttp
from datetime import datetime from datetime import datetime
from fastapi import FastAPI, HTTPException, Depends, Request, Response from fastapi import FastAPI, HTTPException, WebSocket, Depends, Request, Response
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
@@ -101,6 +101,12 @@ SERVICES = {
base_url="http://localhost:8006", base_url="http://localhost:8006",
health_endpoint="/health", health_endpoint="/health",
auth_required=True auth_required=True
),
"sensor-service": ServiceConfig(
name="sensor-service",
base_url="http://localhost:8007",
health_endpoint="/health",
auth_required=True
) )
} }
@@ -204,6 +210,70 @@ async def iot_control_service_proxy(request: Request, path: str):
"""Proxy requests to IoT control service""" """Proxy requests to IoT control service"""
return await proxy_request(request, "iot-control-service", f"/{path}") return await proxy_request(request, "iot-control-service", f"/{path}")
# Sensor Service Routes (Original Dashboard Functionality)
@app.api_route("/api/v1/sensors/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def sensor_service_proxy(request: Request, path: str):
"""Proxy requests to sensor service"""
return await proxy_request(request, "sensor-service", f"/{path}")
@app.api_route("/api/v1/rooms/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def room_service_proxy(request: Request, path: str):
"""Proxy requests to sensor service for room management"""
return await proxy_request(request, "sensor-service", f"/rooms/{path}")
@app.api_route("/api/v1/data/{path:path}", methods=["GET", "POST"])
async def data_service_proxy(request: Request, path: str):
"""Proxy requests to sensor service for data operations"""
return await proxy_request(request, "sensor-service", f"/data/{path}")
@app.api_route("/api/v1/analytics/{path:path}", methods=["GET", "POST"])
async def analytics_service_proxy(request: Request, path: str):
"""Proxy requests to sensor service for analytics"""
return await proxy_request(request, "sensor-service", f"/analytics/{path}")
@app.api_route("/api/v1/export", methods=["GET"])
async def export_service_proxy(request: Request):
"""Proxy requests to sensor service for data export"""
return await proxy_request(request, "sensor-service", "/export")
@app.api_route("/api/v1/events", methods=["GET"])
async def events_service_proxy(request: Request):
"""Proxy requests to sensor service for system events"""
return await proxy_request(request, "sensor-service", "/events")
# WebSocket proxy for real-time data
@app.websocket("/ws")
async def websocket_proxy(websocket: WebSocket):
"""Proxy WebSocket connections to sensor service"""
try:
# Get sensor service URL
service_url = await load_balancer.get_service_url("sensor-service")
if not service_url:
await websocket.close(code=1003, reason="Sensor service unavailable")
return
# For simplicity, we'll just accept the connection and forward to sensor service
# In a production setup, you'd want a proper WebSocket proxy
await websocket.accept()
# For now, we'll handle this by having the sensor service manage WebSockets directly
# The frontend should connect to the sensor service WebSocket endpoint directly
await websocket.send_text(json.dumps({
"type": "proxy_info",
"message": "Connect directly to sensor service WebSocket at /ws",
"sensor_service_url": service_url.replace("http://", "ws://") + "/ws"
}))
# Keep connection alive
while True:
try:
await websocket.receive_text()
except:
break
except Exception as e:
logger.error(f"WebSocket proxy error: {e}")
async def proxy_request(request: Request, service_name: str, path: str): async def proxy_request(request: Request, service_name: str, path: str):
"""Generic request proxy function""" """Generic request proxy function"""
try: try:
@@ -299,6 +369,7 @@ async def get_system_overview():
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
# Try to get service-specific overview data # Try to get service-specific overview data
overview_endpoints = { overview_endpoints = {
"sensor-service": "/analytics/summary",
"battery-service": "/batteries", "battery-service": "/batteries",
"demand-response-service": "/flexibility/current", "demand-response-service": "/flexibility/current",
"p2p-trading-service": "/market/status", "p2p-trading-service": "/market/status",

View File

@@ -42,7 +42,7 @@ check_dependencies() {
exit 1 exit 1
fi fi
if ! command -v docker-compose &> /dev/null; then if ! command -v docker compose &> /dev/null; then
print_error "Docker Compose is not installed. Please install Docker Compose first." print_error "Docker Compose is not installed. Please install Docker Compose first."
exit 1 exit 1
fi fi
@@ -132,7 +132,7 @@ EOF
build_services() { build_services() {
print_status "Building all microservices..." print_status "Building all microservices..."
docker-compose -f $COMPOSE_FILE build docker compose -f $COMPOSE_FILE build
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
print_success "All services built successfully" print_success "All services built successfully"
@@ -146,7 +146,7 @@ build_services() {
start_services() { start_services() {
print_status "Starting all services..." print_status "Starting all services..."
docker-compose -f $COMPOSE_FILE up -d docker compose -f $COMPOSE_FILE up -d
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
print_success "All services started successfully" print_success "All services started successfully"
@@ -160,7 +160,7 @@ start_services() {
stop_services() { stop_services() {
print_status "Stopping all services..." print_status "Stopping all services..."
docker-compose -f $COMPOSE_FILE down docker compose -f $COMPOSE_FILE down
print_success "All services stopped" print_success "All services stopped"
} }
@@ -174,15 +174,15 @@ restart_services() {
# Function to show service status # Function to show service status
show_status() { show_status() {
print_status "Service status:" print_status "Service status:"
docker-compose -f $COMPOSE_FILE ps docker compose -f $COMPOSE_FILE ps
print_status "Service health checks:" print_status "Service health checks:"
# Wait a moment for services to start # Wait a moment for services to start
sleep 5 sleep 5
services=("api-gateway:8000" "token-service:8001" "battery-service:8002" "demand-response-service:8003") # services=("api-gateway:8000" "token-service:8001" "battery-service:8002" "demand-response-service:8003")
services=("api-gateway:8000" "token-service:8001")
for service in "${services[@]}"; do for service in "${services[@]}"; do
name="${service%:*}" name="${service%:*}"
port="${service#*:}" port="${service#*:}"
@@ -199,10 +199,10 @@ show_status() {
view_logs() { view_logs() {
if [ -z "$2" ]; then if [ -z "$2" ]; then
print_status "Showing logs for all services..." print_status "Showing logs for all services..."
docker-compose -f $COMPOSE_FILE logs -f docker compose -f $COMPOSE_FILE logs -f
else else
print_status "Showing logs for $2..." print_status "Showing logs for $2..."
docker-compose -f $COMPOSE_FILE logs -f $2 docker compose -f $COMPOSE_FILE logs -f $2
fi fi
} }
@@ -212,7 +212,7 @@ cleanup() {
read -r response read -r response
if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then
print_status "Cleaning up everything..." print_status "Cleaning up everything..."
docker-compose -f $COMPOSE_FILE down -v --rmi all docker compose -f $COMPOSE_FILE down -v --rmi all
docker system prune -f docker system prune -f
print_success "Cleanup completed" print_success "Cleanup completed"
else else

View File

@@ -1,4 +1,4 @@
version: '3.8' version: "3.8"
services: services:
# Database Services # Database Services
@@ -41,17 +41,19 @@ services:
- MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard?authSource=admin - MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard?authSource=admin
- REDIS_URL=redis://redis:6379 - REDIS_URL=redis://redis:6379
- TOKEN_SERVICE_URL=http://token-service:8001 - TOKEN_SERVICE_URL=http://token-service:8001
- SENSOR_SERVICE_URL=http://sensor-service:8007
- BATTERY_SERVICE_URL=http://battery-service:8002 - BATTERY_SERVICE_URL=http://battery-service:8002
- DEMAND_RESPONSE_SERVICE_URL=http://demand-response-service:8003 - DEMAND_RESPONSE_SERVICE_URL=http://demand-response-service:8003
- P2P_TRADING_SERVICE_URL=http://p2p-trading-service:8004 - P2P_TRADING_SERVICE_URL=http://p2p-trading-service:8004
- FORECASTING_SERVICE_URL=http://forecasting-service:8005 - FORECASTING_SERVICE_URL=http://forecasting-service:8005
- IOT_CONTROL_SERVICE_URL=http://iot-control-service:8006 - IOT_CONTROL_SERVICE_URL=http://iot-control-service:8006
- DATA_INGESTION_SERVICE_URL=http://data-ingestion-service:8008
depends_on: depends_on:
- mongodb - mongodb
- redis - redis
- token-service - token-service
- battery-service # - battery-service
- demand-response-service # - demand-response-service
networks: networks:
- energy-network - energy-network
@@ -73,92 +75,134 @@ services:
- energy-network - energy-network
# Battery Management Service # Battery Management Service
battery-service: # battery-service:
build: # build:
context: ./battery-service # context: ./battery-service
dockerfile: Dockerfile # dockerfile: Dockerfile
container_name: energy-battery-service # container_name: energy-battery-service
restart: unless-stopped # restart: unless-stopped
ports: # ports:
- "8002:8002" # - "8002:8002"
environment: # environment:
- MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_batteries?authSource=admin # - MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_batteries?authSource=admin
- REDIS_URL=redis://redis:6379 # - REDIS_URL=redis://redis:6379
depends_on: # depends_on:
- mongodb # - mongodb
- redis # - redis
networks: # networks:
- energy-network # - energy-network
# Demand Response Service # Demand Response Service
demand-response-service: # demand-response-service:
build: # build:
context: ./demand-response-service # context: ./demand-response-service
dockerfile: Dockerfile # dockerfile: Dockerfile
container_name: energy-demand-response-service # container_name: energy-demand-response-service
restart: unless-stopped # restart: unless-stopped
ports: # ports:
- "8003:8003" # - "8003:8003"
environment: # environment:
- MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_demand_response?authSource=admin # - MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_demand_response?authSource=admin
- REDIS_URL=redis://redis:6379 # - REDIS_URL=redis://redis:6379
- IOT_CONTROL_SERVICE_URL=http://iot-control-service:8006 # - IOT_CONTROL_SERVICE_URL=http://iot-control-service:8006
depends_on: # depends_on:
- mongodb # - mongodb
- redis # - redis
networks: # networks:
- energy-network # - energy-network
# P2P Trading Service # P2P Trading Service
p2p-trading-service: # p2p-trading-service:
build: # build:
context: ./p2p-trading-service # context: ./p2p-trading-service
dockerfile: Dockerfile # dockerfile: Dockerfile
container_name: energy-p2p-trading-service # container_name: energy-p2p-trading-service
restart: unless-stopped # restart: unless-stopped
ports: # ports:
- "8004:8004" # - "8004:8004"
environment: # environment:
- MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_p2p?authSource=admin # - MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_p2p?authSource=admin
- REDIS_URL=redis://redis:6379 # - REDIS_URL=redis://redis:6379
depends_on: # depends_on:
- mongodb # - mongodb
- redis # - redis
networks: # networks:
- energy-network # - energy-network
# Forecasting Service # Forecasting Service
forecasting-service: # forecasting-service:
# build:
# context: ./forecasting-service
# dockerfile: Dockerfile
# container_name: energy-forecasting-service
# restart: unless-stopped
# ports:
# - "8005:8005"
# environment:
# - MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_forecasting?authSource=admin
# - REDIS_URL=redis://redis:6379
# depends_on:
# - mongodb
# - redis
# networks:
# - energy-network
# IoT Control Service
# iot-control-service:
# build:
# context: ./iot-control-service
# dockerfile: Dockerfile
# container_name: energy-iot-control-service
# restart: unless-stopped
# ports:
# - "8006:8006"
# environment:
# - MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_iot?authSource=admin
# - REDIS_URL=redis://redis:6379
# - BATTERY_SERVICE_URL=http://battery-service:8002
# - DEMAND_RESPONSE_SERVICE_URL=http://demand-response-service:8003
# depends_on:
# - mongodb
# - redis
# networks:
# - energy-network
# Data Ingestion Service (FTP Monitoring & SA4CPS Integration)
data-ingestion-service:
build: build:
context: ./forecasting-service context: ./data-ingestion-service
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: energy-forecasting-service container_name: energy-data-ingestion-service
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8005:8005" - "8008:8008"
environment: environment:
- MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_forecasting?authSource=admin - MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_ingestion?authSource=admin
- REDIS_URL=redis://redis:6379 - REDIS_URL=redis://redis:6379
- FTP_SA4CPS_HOST=ftp.sa4cps.pt
- FTP_SA4CPS_PORT=21
- FTP_SA4CPS_USERNAME=anonymous
- FTP_SA4CPS_PASSWORD=
- FTP_SA4CPS_REMOTE_PATH=/
depends_on: depends_on:
- mongodb - mongodb
- redis - redis
networks: networks:
- energy-network - energy-network
# IoT Control Service # Sensor Management Service (Original Dashboard Functionality)
iot-control-service: sensor-service:
build: build:
context: ./iot-control-service context: ./sensor-service
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: energy-iot-control-service container_name: energy-sensor-service
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8006:8006" - "8007:8007"
environment: environment:
- MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_iot?authSource=admin - MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_sensors?authSource=admin
- REDIS_URL=redis://redis:6379 - REDIS_URL=redis://redis:6379
- BATTERY_SERVICE_URL=http://battery-service:8002 - TOKEN_SERVICE_URL=http://token-service:8001
- DEMAND_RESPONSE_SERVICE_URL=http://demand-response-service:8003
depends_on: depends_on:
- mongodb - mongodb
- redis - redis

View File

@@ -1 +0,0 @@
# Services package for dashboard backend

View File

@@ -1,174 +0,0 @@
"""
Token management service for authentication and resource access control.
Based on the tiocps JWT token implementation with resource-based permissions.
"""
import jwt
import uuid
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any
from pydantic import BaseModel
from motor.motor_asyncio import AsyncIOMotorDatabase
class TokenPayload(BaseModel):
"""Token payload structure"""
name: str
list_of_resources: List[str]
data_aggregation: bool = False
time_aggregation: bool = False
embargo: int = 0 # embargo period in seconds
exp: int # expiration timestamp
class TokenRecord(BaseModel):
"""Token database record"""
token: str
datetime: datetime
active: bool = True
created_at: datetime
updated_at: datetime
class TokenService:
"""Service for managing JWT tokens and authentication"""
def __init__(self, db: AsyncIOMotorDatabase, secret_key: str = "dashboard-secret-key"):
self.db = db
self.secret_key = secret_key
self.tokens_collection = db.tokens
def generate_token(self, name: str, list_of_resources: List[str],
data_aggregation: bool = False, time_aggregation: bool = False,
embargo: int = 0, exp_hours: int = 24) -> str:
"""Generate a new JWT token with specified permissions"""
# Calculate expiration time
exp_timestamp = int((datetime.utcnow() + timedelta(hours=exp_hours)).timestamp())
# Create token payload
payload = {
"name": name,
"list_of_resources": list_of_resources,
"data_aggregation": data_aggregation,
"time_aggregation": time_aggregation,
"embargo": embargo,
"exp": exp_timestamp,
"iat": int(datetime.utcnow().timestamp()),
"jti": str(uuid.uuid4()) # unique token ID
}
# Generate JWT token
token = jwt.encode(payload, self.secret_key, algorithm="HS256")
return token
def decode_token(self, token: str) -> Optional[Dict[str, Any]]:
"""Decode and verify JWT token"""
try:
payload = jwt.decode(token, self.secret_key, algorithms=["HS256"])
return payload
except jwt.ExpiredSignatureError:
return {"error": "Token has expired"}
except jwt.InvalidTokenError:
return {"error": "Invalid token"}
async def insert_token(self, token: str) -> Dict[str, Any]:
"""Save token to database"""
now = datetime.utcnow()
# Decode token to verify it's valid
decoded = self.decode_token(token)
if decoded and "error" not in decoded:
token_record = {
"token": token,
"datetime": now,
"active": True,
"created_at": now,
"updated_at": now,
"name": decoded.get("name", ""),
"resources": decoded.get("list_of_resources", []),
"expires_at": datetime.fromtimestamp(decoded.get("exp", 0))
}
await self.tokens_collection.insert_one(token_record)
return {
"token": token,
"datetime": now.isoformat(),
"active": True
}
else:
raise ValueError("Invalid token cannot be saved")
async def revoke_token(self, token: str) -> Dict[str, Any]:
"""Revoke a token by marking it as inactive"""
now = datetime.utcnow()
result = await self.tokens_collection.update_one(
{"token": token},
{
"$set": {
"active": False,
"updated_at": now,
"revoked_at": now
}
}
)
if result.matched_count > 0:
return {
"token": token,
"datetime": now.isoformat(),
"active": False
}
else:
raise ValueError("Token not found")
async def get_tokens(self) -> List[Dict[str, Any]]:
"""Get all tokens from database"""
cursor = self.tokens_collection.find({})
tokens = []
async for token_record in cursor:
# Convert ObjectId to string and datetime to ISO format
token_record["_id"] = str(token_record["_id"])
for field in ["datetime", "created_at", "updated_at", "expires_at", "revoked_at"]:
if field in token_record and token_record[field]:
token_record[field] = token_record[field].isoformat()
tokens.append(token_record)
return tokens
async def is_token_valid(self, token: str) -> bool:
"""Check if token is valid and active"""
# Check if token exists and is active in database
token_record = await self.tokens_collection.find_one({
"token": token,
"active": True
})
if not token_record:
return False
# Verify JWT signature and expiration
decoded = self.decode_token(token)
return decoded is not None and "error" not in decoded
async def get_token_permissions(self, token: str) -> Optional[Dict[str, Any]]:
"""Get permissions for a valid token"""
if await self.is_token_valid(token):
return self.decode_token(token)
return None
async def cleanup_expired_tokens(self):
"""Remove expired tokens from database"""
now = datetime.utcnow()
# Find tokens that have expired
expired_cursor = self.tokens_collection.find({
"expires_at": {"$lt": now}
})
expired_count = 0
async for token_record in expired_cursor:
await self.tokens_collection.delete_one({"_id": token_record["_id"]})
expired_count += 1
return expired_count