Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions backend/app/database/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from motor.motor_asyncio import AsyncIOMotorClient
from app.database.analytics_schema import AnalyticsDB
import os

# Get MongoDB connection string from environment or use default
MONGODB_URL = os.getenv("MONGODB_URL", "mongodb://localhost:27017/")
DB_NAME = os.getenv("MONGODB_DB_NAME", "algorithm_visualizer")

# Create a global client instance
client = AsyncIOMotorClient(MONGODB_URL)
db = client[DB_NAME]

# Initialize collections
analytics_db = AnalyticsDB(db)

# Dependency to get database instance
async def get_database():
return db
47 changes: 47 additions & 0 deletions backend/app/database/analytics_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from pymongo import MongoClient, IndexModel, ASCENDING
from datetime import datetime, timedelta
import os

class AnalyticsDB:
def __init__(self, db):
self.db = db
self.analytics_collection = db.analytics_events
self.feedback_collection = db.user_feedback
self.consent_collection = db.user_consent
self.setup_indexes()

def setup_indexes(self):
# Create indexes for better query performance
analytics_indexes = [
IndexModel([("timestamp", ASCENDING)]),
IndexModel([("event_type", ASCENDING)]),
IndexModel([("user_session", ASCENDING)]),
IndexModel([("algorithm", ASCENDING)]),
]

feedback_indexes = [
IndexModel([("timestamp", ASCENDING)]),
IndexModel([("user_session", ASCENDING)]),
IndexModel([("algorithm", ASCENDING)]),
]

consent_indexes = [
IndexModel([("user_session", ASCENDING)]),
IndexModel([("timestamp", ASCENDING)]),
]

self.analytics_collection.create_indexes(analytics_indexes)
self.feedback_collection.create_indexes(feedback_indexes)
self.consent_collection.create_indexes(consent_indexes)

def clean_old_data(self, days_to_keep=90):
"""Remove data older than specified days for GDPR compliance"""
cutoff_date = datetime.utcnow() - timedelta(days=days_to_keep)

self.analytics_collection.delete_many({
"timestamp": {"$lt": cutoff_date}
})

self.feedback_collection.delete_many({
"timestamp": {"$lt": cutoff_date}
})
199 changes: 199 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import os
from fastapi import FastAPI, Depends, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel, ConfigDict
from typing import List, Optional, Dict, Any
import uvicorn
from datetime import datetime, timedelta

# Import database and analytics
from app.database import get_database, analytics_db
from app.routers import analytics as analytics_router

app = FastAPI(
title="Algorithm Visualizer API",
description="Backend API for Algorithm Visualizer Platform with Analytics",
version="1.0.0"
)

# Environment detection
ENVIRONMENT = os.getenv("ENVIRONMENT", "development")
IS_PRODUCTION = ENVIRONMENT == "production"

# Configure CORS
origins = ["*"] if not IS_PRODUCTION else [
"https://yourdomain.com",
"https://www.yourdomain.com"
]

app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# Include analytics router
app.include_router(analytics_router.router)

# Lazy import services to avoid import issues
_sorting_service = None
_graph_service = None

def get_sorting_service():
global _sorting_service
if _sorting_service is None:
try:
from backend.services.sorting_service import SortingService
_sorting_service = SortingService()
except ImportError:
try:
from services.sorting_service import SortingService
_sorting_service = SortingService()
except ImportError as e:
raise HTTPException(status_code=500, detail=f"Cannot import SortingService: {e}")
return _sorting_service

def get_graph_service():
global _graph_service
if _graph_service is None:
try:
from backend.services.graph_service import GraphService
_graph_service = GraphService()
except ImportError:
try:
from services.graph_service import GraphService
_graph_service = GraphService()
except ImportError as e:
raise HTTPException(status_code=500, detail=f"Cannot import GraphService: {e}")
return _graph_service

# Request models with Pydantic v2 config
class SortingRequest(BaseModel):
model_config = ConfigDict(populate_by_name=True)
array: List[int]

class GraphNodeModel(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: int
label: Optional[str] = ""
x: Optional[float] = 0.0
y: Optional[float] = 0.0

class GraphEdgeModel(BaseModel):
model_config = ConfigDict(populate_by_name=True)
from_node: int
to: int
weight: Optional[float] = 1.0
directed: Optional[bool] = False

class GraphRequest(BaseModel):
model_config = ConfigDict(populate_by_name=True)
nodes: List[GraphNodeModel]
edges: List[GraphEdgeModel]
start_node: Optional[int] = 0
end_node: Optional[int] = None

# Health check endpoint
@app.get("/api/health")
async def health_check():
return {"status": "healthy", "environment": ENVIRONMENT}

# Sorting endpoints
@app.post("/api/sort/{algorithm}")
async def run_sorting_algorithm(algorithm: str, request: SortingRequest):
service = get_sorting_service()
try:
result = service.sort(algorithm, request.array)
return {"sorted_array": result}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

# Graph endpoints
@app.post("/api/graph/{algorithm}")
async def run_graph_algorithm(algorithm: str, request: GraphRequest):
service = get_graph_service()

# Convert request to the format expected by the service
graph_data = {
"nodes": [{"id": node.id, "label": node.label, "x": node.x, "y": node.y}
for node in request.nodes],
"edges": [{"from": edge.from_node, "to": edge.to,
"weight": edge.weight, "directed": edge.directed}
for edge in request.edges],
"start_node": request.start_node,
"end_node": request.end_node
}

try:
result = service.run_algorithm(algorithm, graph_data)
return result
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

# String algorithms placeholder
@app.post("/api/string/{algorithm}")
async def run_string_algorithm(algorithm: str, request: dict):
return {"message": f"String algorithm {algorithm} execution not implemented yet"}

# DP algorithms placeholder
@app.post("/api/dp/{algorithm}")
async def run_dp_algorithm(algorithm: str, request: dict):
return {"message": f"DP algorithm {algorithm} execution not implemented yet"}

# Clean up old analytics data on startup
@app.on_event("startup")
async def startup_event():
try:
# Clean up data older than 90 days
await analytics_db.clean_old_data(days_to_keep=90)
print("Successfully cleaned up old analytics data")
except Exception as e:
print(f"Error during startup cleanup: {e}")

# Check if frontend is built and available
frontend_path = os.path.join(os.path.dirname(__file__), "..", "..", "frontend", "build")
frontend_available = os.path.exists(frontend_path) and os.path.isdir(frontend_path)

if frontend_available:
# Serve static files from the frontend build directory
app.mount("/static", StaticFiles(directory=os.path.join(frontend_path, "static")), name="static")

# Serve the React app for any other route
@app.get("/{full_path:path}")
async def serve_react_app(full_path: str):
# Don't interfere with API routes
if full_path.startswith("api/"):
return {"error": "Not found"}, 404

file_path = os.path.join(frontend_path, full_path)
if os.path.exists(file_path) and os.path.isfile(file_path):
return FileResponse(file_path)

# Serve index.html for all other paths (client-side routing)
return FileResponse(os.path.join(frontend_path, "index.html"))

if __name__ == "__main__":
port = int(os.environ.get("PORT", 8000))
host = "0.0.0.0" if IS_PRODUCTION else "127.0.0.1"

# Print startup information
print(f"Starting server in {ENVIRONMENT} mode")
print(f"Frontend available: {frontend_available}")
print(f"Server running at http://{host}:{port}")

# Start the server
uvicorn.run(
"app.main:app",
host=host,
port=port,
reload=not IS_PRODUCTION,
workers=4 if IS_PRODUCTION else 1
)
36 changes: 36 additions & 0 deletions backend/app/models/analytics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any
from datetime import datetime
from enum import Enum

class EventType(str, Enum):
PAGE_VIEW = "page_view"
ALGORITHM_EXECUTION = "algorithm_execution"
SESSION_START = "session_start"
SESSION_END = "session_end"
FEEDBACK_SUBMITTED = "feedback_submitted"
THEME_CHANGED = "theme_changed"
SPEED_CHANGED = "speed_changed"

class AnalyticsEvent(BaseModel):
event_type: EventType
algorithm: Optional[str] = None
input_size: Optional[int] = None
execution_time: Optional[float] = None
page_url: Optional[str] = None
user_session: str = Field(..., description="Anonymous session hash")
timestamp: datetime = Field(default_factory=datetime.utcnow)
metadata: Optional[Dict[str, Any]] = {}

class UserFeedback(BaseModel):
user_session: str
rating: int = Field(..., ge=1, le=5)
feedback_text: Optional[str] = None
algorithm: Optional[str] = None
timestamp: datetime = Field(default_factory=datetime.utcnow)

class ConsentSettings(BaseModel):
user_session: str
analytics_consent: bool = True
feedback_consent: bool = True
timestamp: datetime = Field(default_factory=datetime.utcnow)
89 changes: 89 additions & 0 deletions backend/app/routers/analytics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from fastapi import APIRouter, Depends, HTTPException, status
from app.models.analytics import AnalyticsEvent, UserFeedback, ConsentSettings
from app.services.analytics_service import AnalyticsService
from app.database import get_database
from typing import Dict, Any

router = APIRouter(prefix="/api/analytics", tags=["analytics"])

async def get_analytics_service(db = Depends(get_database)):
return AnalyticsService(db)

@router.post("/event", status_code=status.HTTP_201_CREATED)
async def log_analytics_event(
event: AnalyticsEvent,
analytics_service: AnalyticsService = Depends(get_analytics_service)
):
"""Log an analytics event"""
success = await analytics_service.log_event(event)
if success:
return {"message": "Event logged successfully"}
else:
return {"message": "Event not logged - no consent or error"}

@router.post("/feedback", status_code=status.HTTP_201_CREATED)
async def submit_feedback(
feedback: UserFeedback,
analytics_service: AnalyticsService = Depends(get_analytics_service)
):
"""Submit user feedback"""
success = await analytics_service.submit_feedback(feedback)
if success:
return {"message": "Feedback submitted successfully"}
else:
return {"message": "Feedback not submitted - no consent or error"}

@router.post("/consent", status_code=status.HTTP_200_OK)
async def update_consent(
consent: ConsentSettings,
analytics_service: AnalyticsService = Depends(get_analytics_service)
):
"""Update user consent settings"""
success = await analytics_service.update_consent(consent)
if success:
return {"message": "Consent updated successfully"}
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update consent"
)

@router.get("/dashboard", response_model=Dict[str, Any])
async def get_analytics_dashboard(
days: int = 30,
analytics_service: AnalyticsService = Depends(get_analytics_service)
):
"""Get analytics dashboard data"""
try:
summary = await analytics_service.get_analytics_summary(days)
return summary
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch analytics data: {str(e)}"
)

@router.delete("/data/{user_session}")
async def delete_user_data(
user_session: str,
analytics_service: AnalyticsService = Depends(get_analytics_service)
):
"""Delete all data for a user session (GDPR compliance)"""
try:
# Delete from all collections
await analytics_service.analytics_db.analytics_collection.delete_many({
"user_session": user_session
})
await analytics_service.analytics_db.feedback_collection.delete_many({
"user_session": user_session
})
await analytics_service.analytics_db.consent_collection.delete_many({
"user_session": user_session
})

return {"message": "User data deleted successfully"}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete user data: {str(e)}"
)
Loading
Loading