diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..98d4abd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Use the custom 'theirs' merge driver for all files to prefer incoming changes. +* merge=theirs diff --git a/data-processing/dataProcessingServer/api/__pycache__/middleware.cpython-313.pyc b/data-processing/dataProcessingServer/api/__pycache__/middleware.cpython-313.pyc new file mode 100644 index 0000000..13ea6eb Binary files /dev/null and b/data-processing/dataProcessingServer/api/__pycache__/middleware.cpython-313.pyc differ diff --git a/data-processing/dataProcessingServer/api/__pycache__/views.cpython-313.pyc b/data-processing/dataProcessingServer/api/__pycache__/views.cpython-313.pyc index ae78703..137df9c 100644 Binary files a/data-processing/dataProcessingServer/api/__pycache__/views.cpython-313.pyc and b/data-processing/dataProcessingServer/api/__pycache__/views.cpython-313.pyc differ diff --git a/data-processing/dataProcessingServer/api/middleware.py b/data-processing/dataProcessingServer/api/middleware.py new file mode 100644 index 0000000..98bcbb1 --- /dev/null +++ b/data-processing/dataProcessingServer/api/middleware.py @@ -0,0 +1,30 @@ +from django.http import JsonResponse +import traceback +from django.conf import settings + + +class ApiExceptionMiddleware: + """ + Catch unhandled exceptions for API paths and return a JSON 500 response. + This avoids triggering Django's HTML technical_500 page (which on some + environments can fail to render) and gives a stable JSON error for the + frontend. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + try: + return self.get_response(request) + except Exception as exc: + # Only intercept API routes + path = getattr(request, 'path', '') or '' + if path.startswith('/api/'): + if settings.DEBUG: + tb = traceback.format_exc() + return JsonResponse({'success': False, 'message': 'Internal server error', 'error': str(exc), 'trace': tb}, status=500) + else: + return JsonResponse({'success': False, 'message': 'Internal server error'}, status=500) + # Re-raise for non-API routes so Django can handle normally + raise diff --git a/data-processing/dataProcessingServer/api/pathway/__pycache__/pipeline.cpython-313.pyc b/data-processing/dataProcessingServer/api/pathway/__pycache__/pipeline.cpython-313.pyc index e104d66..67674fa 100644 Binary files a/data-processing/dataProcessingServer/api/pathway/__pycache__/pipeline.cpython-313.pyc and b/data-processing/dataProcessingServer/api/pathway/__pycache__/pipeline.cpython-313.pyc differ diff --git a/data-processing/dataProcessingServer/api/pathway/pipeline.py b/data-processing/dataProcessingServer/api/pathway/pipeline.py index 71a90b5..7a0a36a 100644 --- a/data-processing/dataProcessingServer/api/pathway/pipeline.py +++ b/data-processing/dataProcessingServer/api/pathway/pipeline.py @@ -29,6 +29,15 @@ except (ImportError, AttributeError): pass +# Pathway is Linux-only. Even if the package is importable on Windows +# (e.g. accidental/bundled install), avoid using it because it can +# hang or fail at runtime on Windows environments. Force disabled on +# Windows to ensure the Django server remains responsive. +import sys +if sys.platform.startswith("win") and PATHWAY_AVAILABLE: + PATHWAY_AVAILABLE = False + pw = None + from .transformers import compute_route_score, compute_batch_scores diff --git a/data-processing/dataProcessingServer/api/views.py b/data-processing/dataProcessingServer/api/views.py index 2fa0149..2562231 100644 --- a/data-processing/dataProcessingServer/api/views.py +++ b/data-processing/dataProcessingServer/api/views.py @@ -71,6 +71,7 @@ def compute_scores(request): routes = body.get("routes", []) use_pathway = body.get("usePathway", False) + # Validation if not isinstance(routes, list): @@ -174,6 +175,7 @@ def compute_single_score(request): # Import here to avoid circular imports from .pathway.transformers import compute_route_score + result = compute_route_score(body) diff --git a/data-processing/dataProcessingServer/dataProcessingServer/__pycache__/settings.cpython-313.pyc b/data-processing/dataProcessingServer/dataProcessingServer/__pycache__/settings.cpython-313.pyc index b7b63a6..1b37519 100644 Binary files a/data-processing/dataProcessingServer/dataProcessingServer/__pycache__/settings.cpython-313.pyc and b/data-processing/dataProcessingServer/dataProcessingServer/__pycache__/settings.cpython-313.pyc differ diff --git a/data-processing/dataProcessingServer/dataProcessingServer/settings.py b/data-processing/dataProcessingServer/dataProcessingServer/settings.py index b37f6b5..9e11f30 100644 --- a/data-processing/dataProcessingServer/dataProcessingServer/settings.py +++ b/data-processing/dataProcessingServer/dataProcessingServer/settings.py @@ -68,12 +68,21 @@ def _normalize_host(value: str) -> str: os.getenv('RENDER_EXTERNAL_HOSTNAME', ''), os.getenv('RENDER_EXTERNAL_URL', ''), os.getenv('RENDER_SERVICE_NAME', ''), + os.getenv('RAILWAY_PUBLIC_DOMAIN', ''), + os.getenv('BACKEND_URL', ''), + os.getenv('API_URL', ''), + os.getenv('PUBLIC_URL', ''), + os.getenv('APP_URL', ''), ] _render_hosts = [ h for h in (_normalize_host(c) for c in _render_host_candidates) if h ] -if _allowed_hosts_from_env: +_allow_all_hosts = os.getenv('DJANGO_ALLOW_ALL_HOSTS', '').lower() in ('true', '1', 'yes') + +if _allow_all_hosts: + ALLOWED_HOSTS = ['*'] +elif _allowed_hosts_from_env: ALLOWED_HOSTS = _allowed_hosts_from_env else: # Local dev: allow all localhost variants + Render hosts for production @@ -103,6 +112,8 @@ def _normalize_host(value: str) -> str: ] MIDDLEWARE = [ + # Custom API exception middleware — placed early so API errors return JSON + 'api.middleware.ApiExceptionMiddleware', # CorsMiddleware MUST come before CommonMiddleware to handle preflight OPTIONS requests 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', @@ -144,8 +155,17 @@ def _normalize_host(value: str) -> str: _cors_env = os.getenv('CORS_ALLOWED_ORIGINS', '') _cors_from_env = [o.strip() for o in _cors_env.split(',') if o.strip()] +_cors_url_candidates = [ + os.getenv('FRONTEND_URL', ''), + os.getenv('CLIENT_URL', ''), + os.getenv('WEB_URL', ''), +] +_cors_from_candidates = [u.strip() for u in _cors_url_candidates if u and u.strip()] + if _cors_from_env: CORS_ALLOWED_ORIGINS = _cors_from_env +elif _cors_from_candidates: + CORS_ALLOWED_ORIGINS = _cors_from_candidates else: # Local dev fallback — never reaches production if env var is set CORS_ALLOWED_ORIGINS = [ diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..ff979cb --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,21 @@ +This repository includes a custom merge driver that prefers the incoming ("theirs") side during merges. + +Setup + +- Unix / Git Bash: + - Run: `./scripts/setup-merge-theirs.sh` +- Windows PowerShell: + - Run: `./scripts/setup-merge-theirs.ps1` + +Notes + +- After registering the driver, Git will use the 'theirs' version for all files during merges. +- To accept incoming changes for an ongoing conflict manually: + +```powershell +git checkout --theirs -- . +git add -A +git commit -m "Accept incoming changes (theirs) for merge" +``` + +Be cautious: this will overwrite local changes with incoming ones for files that conflict. diff --git a/scripts/git-merge-theirs.ps1 b/scripts/git-merge-theirs.ps1 new file mode 100644 index 0000000..fe58842 --- /dev/null +++ b/scripts/git-merge-theirs.ps1 @@ -0,0 +1,7 @@ +Param($O, $A, $B) +if (-not (Test-Path $B)) { + Write-Error "git-merge-theirs.ps1: missing 'their' file" + exit 1 +} +Get-Content -Raw -LiteralPath $B | Set-Content -LiteralPath $A -Encoding UTF8 +exit 0 diff --git a/scripts/git-merge-theirs.sh b/scripts/git-merge-theirs.sh new file mode 100644 index 0000000..b31278c --- /dev/null +++ b/scripts/git-merge-theirs.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# git merge driver that prefers "theirs" (incoming) during merges. +# Called with: %O %A %B +if [ -z "$3" ]; then + echo "git-merge-theirs: missing 'their' file parameter" >&2 + exit 1 +fi +cat "$3" > "$2" +exit 0 diff --git a/scripts/setup-merge-theirs.ps1 b/scripts/setup-merge-theirs.ps1 new file mode 100644 index 0000000..a03727e --- /dev/null +++ b/scripts/setup-merge-theirs.ps1 @@ -0,0 +1,4 @@ +Write-Host "Registering merge.theirs driver..." +git config --local merge.theirs.name "Keep theirs (incoming) during merges" +git config --local merge.theirs.driver ".\scripts\git-merge-theirs.ps1 %O %A %B" +Write-Host "Registered merge.theirs driver (local git config)." diff --git a/scripts/setup-merge-theirs.sh b/scripts/setup-merge-theirs.sh new file mode 100644 index 0000000..7169731 --- /dev/null +++ b/scripts/setup-merge-theirs.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# Register the 'theirs' merge driver locally and make scripts executable. +git config --local merge.theirs.name "Keep theirs (incoming) during merges" +git config --local merge.theirs.driver "./scripts/git-merge-theirs.sh %O %A %B" +chmod +x ./scripts/git-merge-theirs.sh || true +chmod +x ./scripts/git-merge-theirs.ps1 || true +echo "Registered merge.theirs driver (local git config)." diff --git a/server/src/Schema/breakPoints.ts b/server/src/Schema/breakPoints.ts index cfd89a0..3855031 100644 --- a/server/src/Schema/breakPoints.ts +++ b/server/src/Schema/breakPoints.ts @@ -1,20 +1,16 @@ import mongoose, { Document, Schema } from "mongoose"; -// --- Main BreakPoint Interface --- - export interface IBreakPoint extends Document { - routeId: mongoose.Types.ObjectId; // Reference to the parent Route document - routeOptionIndex: number; // Index of the specific route option (0, 1, 2...) - pointIndex: number; // Index of this point in the sequence (0, 1, 2...) + routeId: mongoose.Types.ObjectId; + routeOptionIndex: number; + pointIndex: number; location: { type: "Point"; - coordinates: [number, number]; // [longitude, latitude] + coordinates: [number, number]; }; } -// --- Schema --- - const pointSchema = new Schema( { type: { @@ -53,10 +49,8 @@ const breakPointSchema = new Schema( { timestamps: true } ); -// Index for geospatial queries (finding points near a location) breakPointSchema.index({ location: "2dsphere" }); -// Index for retrieving all points for a specific route option breakPointSchema.index({ routeId: 1, routeOptionIndex: 1 }); const BreakPoint = mongoose.model("BreakPoint", breakPointSchema); diff --git a/server/src/Schema/route.schema.ts b/server/src/Schema/route.schema.ts index 4cacbbc..979f76e 100644 --- a/server/src/Schema/route.schema.ts +++ b/server/src/Schema/route.schema.ts @@ -1,10 +1,8 @@ import mongoose, { Document, Schema } from "mongoose"; -/* ================= GEO TYPES ================= */ - export interface IPoint { type: "Point"; - coordinates: [number, number]; // [longitude, latitude] + coordinates: [number, number]; } export interface ITravelMode { @@ -16,21 +14,16 @@ export interface ILineString { coordinates: [number, number][]; } -/* ================= ROUTE OPTION ================= */ - export interface IRouteOption { - distance: number; // in km - duration: number; // in minutes + distance: number; + duration: number; travelMode: ITravelMode; routeGeometry: ILineString; - // dynamically updated by pollution engine lastComputedScore?: number; lastComputedAt?: Date; } -/* ================= MAIN ROUTE ================= */ - export interface IRoute extends Document { userId: mongoose.Types.ObjectId; name?: string; @@ -54,8 +47,6 @@ export interface IRoute extends Document { updatedAt: Date; } -/* ================= SCHEMAS ================= */ - const pointSchema = new Schema( { type: { @@ -86,8 +77,6 @@ const lineStringSchema = new Schema( { _id: false } ); -/* ===== Route Option Schema ===== */ - const routeOptionSchema = new Schema( { distance: { @@ -122,8 +111,6 @@ const routeOptionSchema = new Schema( { _id: false } ); -/* ===== Main Route Schema ===== */ - const routeSchema = new Schema( { userId: { @@ -178,18 +165,12 @@ const routeSchema = new Schema( { timestamps: true } ); -/* ================= INDEXES ================= */ - -// Needed for geo queries (AQI lookup, weather zone) routeSchema.index({ "from.location": "2dsphere" }); routeSchema.index({ "to.location": "2dsphere" }); routeSchema.index({ "routes.routeGeometry": "2dsphere" }); -// User queries routeSchema.index({ userId: 1, updatedAt: -1 }); routeSchema.index({ userId: 1, isFavorite: 1 }); -/* ================= MODEL ================= */ - const Route = mongoose.model("Route", routeSchema); export default Route; diff --git a/server/src/controllers/savedRoutes.controllers.ts b/server/src/controllers/savedRoutes.controllers.ts index 66e9be8..43a1145 100644 --- a/server/src/controllers/savedRoutes.controllers.ts +++ b/server/src/controllers/savedRoutes.controllers.ts @@ -62,7 +62,6 @@ export const saveRoute = async (req: Request, res: Response): Promise => { return; } - // 1. Create the Parent Route Document const newRoute = await Route.create({ userId, name, @@ -72,12 +71,9 @@ export const saveRoute = async (req: Request, res: Response): Promise => { isFavorite: isFavorite ?? false, }); - // 2. Prepare BreakPoints let routeBreakpointsData: RouteBreakpoints[] = []; - // Try to get from Cache first if (searchId) { - // Validate searchId format to prevent cache-key injection (expecting UUID) const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9-a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; @@ -86,66 +82,56 @@ export const saveRoute = async (req: Request, res: Response): Promise => { const cached = (await redis.get(`route_search:${searchId}`)) as { breakpoints: RouteBreakpoints[]; } | null; - // Upstash Redis usually returns the object directly if stored as JSON + if (cached && cached.breakpoints) { routeBreakpointsData = cached.breakpoints; } } catch { - console.warn("[Cache] Redis get failed for validated searchId."); + /* redis miss — fall through to recompute */ } - } else { - console.warn("[Cache] Ignored invalid searchId format."); } } - // Fallback: Re-compute if cache miss or no searchId - if (!routeBreakpointsData || routeBreakpointsData.length === 0) { + if (routeBreakpointsData.length === 0) { try { - // Dynamically import to avoid circular dep issues if any, - // though static import is fine here too. const { computeBreakpoints } = await import("../utils/compute/breakPoint.compute.js"); routeBreakpointsData = computeBreakpoints(routes) as RouteBreakpoints[]; } catch (e) { console.error("Failed to re-compute breakpoints during save:", e); - // If this fails, we save the route without breakpoints rather than crashing } } - // 3. Save BreakPoint Documents - if (routeBreakpointsData && routeBreakpointsData.length > 0) { + if (routeBreakpointsData.length > 0) { const breakPointDocs: BreakpointDoc[] = []; - // Iterate through each route option (0, 1, 2) - routeBreakpointsData.forEach( - (rb: RouteBreakpoints, routeIndex: number) => { - // Iterate through points in this route (point_1, point_2...) - Object.keys(rb).forEach((key) => { - if (key.startsWith("point_")) { - const parts = key.split("_"); - if (parts.length < 2) return; - const idxStr = parts[1]; - if (!idxStr) return; - const pointIndex = parseInt(idxStr, 10) - 1; // 1-based to 0-based - if (Number.isNaN(pointIndex)) return; - - const coord = rb[key]; // { lat, lon } - - if (coord) { - breakPointDocs.push({ - routeId: newRoute._id, - routeOptionIndex: routeIndex, - pointIndex: pointIndex, - location: { - type: "Point", - coordinates: [coord.lon, coord.lat], // GeoJSON: [lon, lat] - }, - }); - } - } - }); - } - ); + routeBreakpointsData.forEach((rb, routeIndex) => { + Object.keys(rb).forEach((key) => { + if (!key.startsWith("point_")) return; + + const parts = key.split("_"); + if (parts.length < 2) return; + + const idxStr = parts[1]; + if (!idxStr) return; + + const pointIndex = parseInt(idxStr, 10) - 1; + if (Number.isNaN(pointIndex)) return; + + const coord = rb[key]; + if (coord) { + breakPointDocs.push({ + routeId: newRoute._id, + routeOptionIndex: routeIndex, + pointIndex, + location: { + type: "Point", + coordinates: [coord.lon, coord.lat], + }, + }); + } + }); + }); if (breakPointDocs.length > 0) { await BreakPoint.insertMany(breakPointDocs); @@ -178,7 +164,6 @@ export const deleteRoute = async ( return; } - // Cascade-delete associated breakpoints await BreakPoint.deleteMany({ routeId: route._id }); res.status(200).json({ success: true, message: "Route deleted" }); diff --git a/server/src/controllers/score.controller.ts b/server/src/controllers/score.controller.ts index c742d9a..ec0c8cb 100644 --- a/server/src/controllers/score.controller.ts +++ b/server/src/controllers/score.controller.ts @@ -13,7 +13,6 @@ import { } from "../utils/compute/weather.compute.js"; import redis from "../utils/redis.js"; -// Type definitions for the request interface RouteGeometry { type: string; coordinates: [number, number][]; @@ -30,20 +29,20 @@ interface RouteData { interface ScoreRequestBody { routes: RouteData[]; - traffic?: number[]; // Traffic score for each route (optional) + traffic?: number[]; } interface WeatherScore { - temperature: number; // 0-100 (optimal: 15-25°C) - humidity: number; // 0-100 (optimal: 30-60%) - pressure: number; // 0-100 (optimal: 1010-1020 hPa) - overall: number; // Average of all weather metrics + temperature: number; + humidity: number; + pressure: number; + overall: number; } interface AQIScore { - aqi: number; // Raw AQI value - score: number; // 0-100 (lower AQI = better score) - category: string; // Good, Moderate, Unhealthy, etc. + aqi: number; + score: number; + category: string; } interface RouteScore { @@ -77,84 +76,50 @@ interface RouteScore { } | undefined; trafficScore: number; - overallScore: number; // Weighted combination of weather, AQI, and traffic + overallScore: number; lastComputedScore?: number | undefined; - scoreChange?: number | undefined; // Difference from last computed score + scoreChange?: number | undefined; } -/** - * Calculate weather score based on temperature - * Optimal: 21°C - * Stricter curve: Deviating by 5°C drops score to ~70 - */ +// Ideal: 21°C — score drops 6 pts per degree away from ideal function calculateTemperatureScore(temp: number): number { - const optimal = 21; - const diff = Math.abs(temp - optimal); - - // Perfect range: +/- 1°C + const diff = Math.abs(temp - 21); if (diff <= 1) return 100; - - // Stricter penalty: loss of 6 points per degree deviation - // Example: 26°C (diff 5) -> 100 - (5 * 6) = 70 - // Example: 31°C (diff 10) -> 100 - (10 * 6) = 40 return Math.max(0, 100 - diff * 6); } -/** - * Calculate weather score based on humidity - * Optimal: 50% - * Stricter curve: +/- 5% is optimal - */ +// Ideal: 45–55% humidity function calculateHumidityScore(humidity: number): number { - // Perfect range: 45-55% if (humidity >= 45 && humidity <= 55) return 100; - - const ideal = 50; - const diff = Math.abs(humidity - ideal); - - // Penalty: loss of 1.5 points per percent deviation outside optimal - return Math.max(0, 100 - (diff - 5) * 2); + return Math.max(0, 100 - (Math.abs(humidity - 50) - 5) * 2); } -/** - * Calculate weather score based on pressure - * Optimal: 1013 hPa - */ +// Ideal: 1013 hPa (sea-level standard) function calculatePressureScore(pressure: number): number { - const optimal = 1013; - const diff = Math.abs(pressure - optimal); - + const diff = Math.abs(pressure - 1013); if (diff <= 2) return 100; return Math.max(0, 100 - (diff - 2) * 4); } -/** - * Calculate overall weather score for a route - */ function calculateWeatherScore(weatherData: RouteWeatherResult): WeatherScore { let totalTemp = 0; let totalHumidity = 0; let totalPressure = 0; let validPoints = 0; - // Aggregate weather data from all points for (const point of weatherData.points) { if (point.main) { - const main = point.main; - totalTemp += calculateTemperatureScore(main.temp); - totalHumidity += calculateHumidityScore(main.humidity); - totalPressure += calculatePressureScore(main.pressure); + totalTemp += calculateTemperatureScore(point.main.temp); + totalHumidity += calculateHumidityScore(point.main.humidity); + totalPressure += calculatePressureScore(point.main.pressure); validPoints++; } } - // Calculate averages const tempScore = validPoints > 0 ? totalTemp / validPoints : 0; const humidityScore = validPoints > 0 ? totalHumidity / validPoints : 0; const pressureScore = validPoints > 0 ? totalPressure / validPoints : 0; - - // Overall weather score (weighted average) - // Temperature is most perceptible to humans, so higher weight + // Weighted blend: temp 50%, humidity 30%, pressure 20% const overall = tempScore * 0.5 + humidityScore * 0.3 + pressureScore * 0.2; return { @@ -165,23 +130,12 @@ function calculateWeatherScore(weatherData: RouteWeatherResult): WeatherScore { }; } -/** - * Calculate AQI score based on Air Quality Index - * AQI Scale (Stricter): - * 0-20: Excellent (100) - * 21-50: Good (100 -> 80 gradient) - * 51-100: Moderate (80 -> 50 gradient) - * 101-150: Unhealthy for Sensitive (50 -> 30) - * 151+: Unhealthy (30 -> 0) - */ function calculateAQIScore(aqiData: RouteAQIResult): AQIScore { let totalAQI = 0; let validPoints = 0; - // Aggregate AQI data from all points for (const point of aqiData.points) { if (point.aqi && point.aqi.aqi !== undefined) { - // Validate AQI is a number const val = Number(point.aqi.aqi); if (!isNaN(val)) { totalAQI += val; @@ -190,20 +144,11 @@ function calculateAQIScore(aqiData: RouteAQIResult): AQIScore { } } - // CRITICAL FIX: If no valid data, do NOT default to 0 (which would score 100) if (validPoints === 0) { - console.warn("⚠️ WARNING: No valid AQI data found! Defaulting to 0 score."); - return { - aqi: 0, - score: 0, // Default to 0, not 100 - category: "Unknown - No Data", - }; + return { aqi: 0, score: 0, category: "Unknown - No Data" }; } - // Calculate average AQI const avgAQI = totalAQI / validPoints; - - // Calculate score based on AQI value (inverted - lower AQI is better) let score = 0; let category = "Unknown"; @@ -211,28 +156,18 @@ function calculateAQIScore(aqiData: RouteAQIResult): AQIScore { score = 100; category = "Excellent"; } else if (avgAQI <= 50) { - // Gradient 100 -> 80 - // Range size: 30 (50-20) - // Score drop: 20 points score = 100 - ((avgAQI - 20) / 30) * 20; category = "Good"; } else if (avgAQI <= 100) { - // Gradient 80 -> 50 - // Range size: 50 - // Score drop: 30 points score = 80 - ((avgAQI - 50) / 50) * 30; category = "Moderate"; } else if (avgAQI <= 150) { - // Gradient 50 -> 30 score = 50 - ((avgAQI - 100) / 50) * 20; category = "Unhealthy for Sensitive Groups"; } else if (avgAQI <= 200) { - // Gradient 30 -> 10 score = 30 - ((avgAQI - 150) / 50) * 20; category = "Unhealthy"; } else { - // 200+ is basically 0 - // Drop from 10 to 0 between 200 and 300 score = Math.max(0, 10 - ((avgAQI - 200) / 100) * 10); category = avgAQI <= 300 ? "Very Unhealthy" : "Hazardous"; } @@ -244,38 +179,17 @@ function calculateAQIScore(aqiData: RouteAQIResult): AQIScore { }; } -/** - * Calculate traffic score (normalize to 0-100 scale) - * Lower traffic value = better score - * Stricter: Sensitivity increased. Even light traffic penalizes score. - */ +// Non-linear penalty curve — light traffic barely penalised, heavy traffic punished function calculateTrafficScore(trafficValue: number): number { - // trafficValue: 0 (clear) to 10 (severe) - // Usually mapped from trafficFactor: (factor - 1) * 3 or similar - // Let's assume input 0-3 range from request body logic - if (trafficValue <= 0) return 100; - - // Non-linear penalty. Small traffic hurts, large traffic kills score. - // 0.5 (light) -> 85 - // 1.0 (moderate) -> 65 - // 2.0 (heavy) -> 25 - // 3.0 (severe) -> 0 - - // Power curve to maintain high score only for VERY clear roads - const normalized = Math.min(trafficValue / 3, 1); // 0 to 1 - const penalty = Math.pow(normalized, 0.7); // Convex curve - - const score = Math.round((1 - penalty) * 100 * 10) / 10; - - return score; + const penalty = Math.pow(Math.min(trafficValue / 3, 1), 0.7); + return Math.round((1 - penalty) * 100 * 10) / 10; } -const SCORE_CACHE_TTL = 1800; // 30 minutes +// Cache computed scores for 30 minutes +const SCORE_CACHE_TTL = 1800; -/** - * Generate a deterministic cache key from route inputs - */ +// SHA-256 hash of route coords + mode + traffic → deterministic cache key function generateScoreCacheKey(routes: RouteData[], traffic: number[]): string { const keyData = routes.map((r, i) => ({ coords: r.routeGeometry.coordinates, @@ -289,15 +203,13 @@ function generateScoreCacheKey(routes: RouteData[], traffic: number[]): string { return `score_cache:${hash}`; } -/** - * Main controller for computing route scores - * Pipeline: Route Data → Breakpoints → Weather → AQI → Score - */ -export const getScoreController = async (req: Request, res: Response) => { +export const getScoreController = async ( + req: Request, + res: Response +): Promise => { try { const { routes, traffic = [] }: ScoreRequestBody = req.body; - // Validation if (!routes || !Array.isArray(routes) || routes.length === 0) { res.status(400).json({ success: false, @@ -308,14 +220,12 @@ export const getScoreController = async (req: Request, res: Response) => { } if (routes.length > 3) { - res.status(400).json({ - success: false, - message: "Maximum 3 routes allowed.", - }); + res + .status(400) + .json({ success: false, message: "Maximum 3 routes allowed." }); return; } - // Validate route data for (let i = 0; i < routes.length; i++) { const route = routes[i]; if ( @@ -332,12 +242,10 @@ export const getScoreController = async (req: Request, res: Response) => { } } - // Check cache for previously computed scores const cacheKey = generateScoreCacheKey(routes, traffic); try { const cached = await redis.get>(cacheKey); if (cached) { - // Generate fresh searchId so breakpoints are available for saving const searchId = uuidv4(); const breakpoints = computeBreakpoints(routes); try { @@ -349,8 +257,8 @@ export const getScoreController = async (req: Request, res: Response) => { }), { ex: 3600 } ); - } catch (e) { - console.error("Redis breakpoint cache failed:", e); + } catch { + /* redis set failed — serve response without caching searchId */ } res.json({ @@ -361,20 +269,14 @@ export const getScoreController = async (req: Request, res: Response) => { }); return; } - } catch (cacheError) { - console.error("Redis score cache read failed:", cacheError); + } catch { + /* redis get failed — skip cache, compute fresh */ } - // Step 1: Extract breakpoints from route geometry const breakpoints = computeBreakpoints(routes); - - // Step 2: Fetch weather data for all breakpoints const weatherData = await computeWeather(breakpoints); - - // Step 3: Fetch AQI data for all breakpoints const aqiData = await computeAQI(breakpoints); - // Step 4: Calculate scores for each route const routeScores: RouteScore[] = routes.map((route, index) => { const routeWeather = weatherData[index]; if (!routeWeather) { @@ -389,19 +291,12 @@ export const getScoreController = async (req: Request, res: Response) => { totalPoints: 0, successfulFetches: 0, } as RouteAQIResult); - if (!aqiData[index]) { - console.warn(`AQI data missing for route ${index}. Using fallback.`); - } const aqiScore = calculateAQIScore(routeAQI); - const trafficValue = traffic[index] || 0; - - // Calculate individual scores const weatherScore = calculateWeatherScore(routeWeather); const trafficScore = calculateTrafficScore(trafficValue); - // Extract detailed weather data let avgTemp = 0; let avgHumidity = 0; let avgPressure = 0; @@ -427,24 +322,25 @@ export const getScoreController = async (req: Request, res: Response) => { } : undefined; - // Extract detailed AQI data (pollutants) - let pm25Total = 0, - pm10Total = 0, - o3Total = 0; - let no2Total = 0, - so2Total = 0, - coTotal = 0; - let pm25Count = 0, - pm10Count = 0, - o3Count = 0; - let no2Count = 0, - so2Count = 0, - coCount = 0; - let dominentpol: string | undefined = undefined; + let pm25Total = 0; + let pm10Total = 0; + let o3Total = 0; + let no2Total = 0; + let so2Total = 0; + let coTotal = 0; + let pm25Count = 0; + let pm10Count = 0; + let o3Count = 0; + let no2Count = 0; + let so2Count = 0; + let coCount = 0; + let dominentpol: string | undefined; for (const point of routeAQI.points) { if (point.aqi) { - if (point.aqi.dominentpol) dominentpol = point.aqi.dominentpol; + if (point.aqi.dominentpol) { + dominentpol = point.aqi.dominentpol; + } if (point.aqi.iaqi?.pm25) { pm25Total += point.aqi.iaqi.pm25.v; pm25Count++; @@ -498,8 +394,7 @@ export const getScoreController = async (req: Request, res: Response) => { }, }; - // Calculate overall score (weighted combination) - // Weather: 40%, AQI: 30%, Traffic: 30% + // Final score: weather 40%, air quality 30%, traffic 30% const overallScore = Math.round( (weatherScore.overall * 0.4 + @@ -508,8 +403,7 @@ export const getScoreController = async (req: Request, res: Response) => { 10 ) / 10; - // Calculate score change if previous score exists - let scoreChange: number | undefined = undefined; + let scoreChange: number | undefined; if (route.lastComputedScore !== undefined) { scoreChange = Math.round((overallScore - route.lastComputedScore) * 10) / 10; @@ -532,12 +426,10 @@ export const getScoreController = async (req: Request, res: Response) => { }; }); - // Find the best route (highest overall score) const bestRoute = routeScores.reduce((best, current) => current.overallScore > best.overallScore ? current : best ); - // Build response data (without searchId/timestamp, for caching) const responseData = { success: true, message: "Route scores computed successfully", @@ -563,7 +455,6 @@ export const getScoreController = async (req: Request, res: Response) => { }, }; - // Cache the computed scores (30 min TTL) const searchId = uuidv4(); try { await Promise.all([ @@ -572,15 +463,12 @@ export const getScoreController = async (req: Request, res: Response) => { }), redis.set( `route_search:${searchId}`, - JSON.stringify({ - breakpoints, - timestamp: new Date().toISOString(), - }), + JSON.stringify({ breakpoints, timestamp: new Date().toISOString() }), { ex: 3600 } ), ]); - } catch (redisError) { - console.error("Redis caching failed (continuing anyway):", redisError); + } catch { + /* redis set failed — response still sent */ } res.json({ @@ -588,11 +476,9 @@ export const getScoreController = async (req: Request, res: Response) => { searchId, timestamp: new Date().toISOString(), }); - } catch (error) { - console.error("Error in getScoreController:", error); - res.status(500).json({ - success: false, - message: "Failed to compute route scores", - }); + } catch { + res + .status(500) + .json({ success: false, message: "Failed to compute route scores" }); } }; diff --git a/server/src/index.ts b/server/src/index.ts index e5ea54e..1019306 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -35,7 +35,6 @@ app.get("/", (_req, res) => { res.json({ message: "Server is running" }); }); -// Manual trigger for batch scoring (admin only) app.post("/api/v1/scheduler/run", tokenVerify, async (_req, res) => { try { await runManualBatchScoring(); @@ -48,7 +47,6 @@ app.post("/api/v1/scheduler/run", tokenVerify, async (_req, res) => { } }); -// Health check for Pathway server (admin only) app.get("/api/v1/scheduler/pathway-health", tokenVerify, async (_req, res) => { const pathwayUrl = process.env.PATHWAY_URL || "http://localhost:8001"; const isHealthy = await checkPathwayHealth(pathwayUrl); @@ -62,10 +60,8 @@ connectDB() app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); - // Start the periodic batch scoring scheduler initScheduler(); - // Also fire once immediately on startup (no wait for first cron tick) runManualBatchScoring().catch((err) => console.error("[Scheduler] Startup run failed:", err) ); diff --git a/server/src/routes/savedRoutes.routes.ts b/server/src/routes/savedRoutes.routes.ts index 3c9b35a..2dae29e 100644 --- a/server/src/routes/savedRoutes.routes.ts +++ b/server/src/routes/savedRoutes.routes.ts @@ -10,7 +10,6 @@ import { tokenVerify } from "../middleware/tokenVerify.js"; const router = Router(); -// All routes require authentication router.use(tokenVerify); router.get("/", fetchSavedRoutes); diff --git a/server/src/routes/score.routes.ts b/server/src/routes/score.routes.ts index 53a123d..595f64b 100644 --- a/server/src/routes/score.routes.ts +++ b/server/src/routes/score.routes.ts @@ -6,7 +6,6 @@ import { tokenVerify } from "../middleware/tokenVerify.js"; const router = Router(); -// Rate limiter: 10 requests per minute per IP const scoreLimiter = rateLimit({ windowMs: 1 * 60 * 1000, // 1 minute max: 10, // Limit each IP to 10 requests per windowMs diff --git a/server/src/utils/compute/aqi.compute.ts b/server/src/utils/compute/aqi.compute.ts index 2389b26..ef54c0b 100644 --- a/server/src/utils/compute/aqi.compute.ts +++ b/server/src/utils/compute/aqi.compute.ts @@ -1,15 +1,10 @@ import type { Coordinate, RoutePoints } from "./weather.compute.js"; -// -// Type definitions for AQI data - -// AQI data structure based on AQICN API response export interface AQIData { - aqi: number; // Air Quality Index value - dominentpol?: string | undefined; // Dominant pollutant (pm25, pm10, o3, etc.) + aqi: number; + dominentpol?: string | undefined; iaqi?: | { - // Individual Air Quality Index for each pollutant pm25?: { v: number }; pm10?: { v: number }; o3?: { v: number }; @@ -20,8 +15,8 @@ export interface AQIData { | undefined; time?: | { - s: string; // ISO timestamp - tz: string; // Timezone + s: string; + tz: string; } | undefined; } @@ -70,11 +65,7 @@ export interface RouteAQIResult { const AQI_TIMEOUT_MS = 5000; const AQI_MAX_RETRIES = 2; -/** - * Fetches AQI data for a single coordinate point using AQICN API. - * Retries up to AQI_MAX_RETRIES times on network failures. - * Returns AQI data or null if all attempts fail. - */ +// Fetches AQI data for a coordinate point using AQICN API with retries async function fetchAQIForPoint( lat: number, lon: number @@ -143,12 +134,7 @@ async function fetchAQIForPoint( return null; } -/** - * Computes AQI data for multiple routes with multiple points - * - * @param routes - Array of route objects, each containing point_1, point_2, point_3, etc. - * @returns Array of AQI results for each route - */ +// Computes AQI data for multiple routes export async function computeAQI( routes: RoutePoints[] ): Promise { @@ -161,14 +147,12 @@ export async function computeAQI( throw new Error("AQI_API_KEY not configured"); } - // Task definition for batching interface PointTask { routeIndex: number; pointKey: string; point: Coordinate; } - // Step 1: Collect ALL points that need AQI data from ALL routes const allPointTasks: PointTask[] = []; routes.forEach((route, routeIndex) => { for (let i = 1; i <= 7; i++) { @@ -180,8 +164,7 @@ export async function computeAQI( } }); - // Step 2: Execute fetches in batches (Concurrency Limiter) - // No more than N requests in parallel to avoid quota/resource issues + // 2. Fetch data in parallel batches (Concurrency Limiter) const pointResultsMap = new Map(); const CONCURRENCY_LIMIT = 5; @@ -219,7 +202,7 @@ export async function computeAQI( }); } - // Step 3: Format the results back to the expected RouteAQIResult[] structure + // 3. Format results into RouteAQIResult structure const results: RouteAQIResult[] = routes.map((_, routeIndex) => { const pointResults = pointResultsMap.get(routeIndex) || []; return { diff --git a/server/src/utils/compute/breakPoint.compute.ts b/server/src/utils/compute/breakPoint.compute.ts index 0bc84d8..37a8565 100644 --- a/server/src/utils/compute/breakPoint.compute.ts +++ b/server/src/utils/compute/breakPoint.compute.ts @@ -1,4 +1,3 @@ -// Type definitions interface Coordinate { lat: number; lon: number; @@ -6,11 +5,11 @@ interface Coordinate { interface RouteGeometry { type: string; - coordinates: [number, number][]; // [lon, lat] format from GeoJSON + coordinates: [number, number][]; } interface RouteInput { - distance: number; // in kilometers + distance: number; duration: number; routeGeometry: RouteGeometry; } @@ -25,27 +24,18 @@ interface RouteBreakpoints { point_7?: Coordinate; } -/** - * Calculate the number of breakpoints based on route distance - * - Under 100 km: 3 points - * - 100-500 km: 3-4 points - * - Above 500 km: 3-4 points (reduced to prevent API timeouts) - */ +// Calculates points needed based on distance to balance detail and speed function calculateBreakpointCount(distance: number): number { if (distance < 100) { return 3; } else if (distance >= 100 && distance <= 500) { - // Scale between 3-4 based on distance return distance < 300 ? 3 : 4; } else { - // For very long routes, use 3-4 points to prevent timeouts return distance < 750 ? 3 : 4; } } -/** - * Check if two coordinates are the same (within a small tolerance) - */ +// Checks if coordinates are within 0.0001 degrees (tolerance) function areCoordinatesEqual( coord1: Coordinate, coord2: Coordinate, @@ -57,9 +47,7 @@ function areCoordinatesEqual( ); } -/** - * Extract evenly spaced breakpoints from a route's coordinates - */ +// Extracts evenly spaced, unique breakpoints from route geometry function extractBreakpoints( coordinates: [number, number][], count: number, @@ -68,24 +56,19 @@ function extractBreakpoints( ): Coordinate[] { const totalCoords = coordinates.length; if (totalCoords < 2) { - return []; // Should be caught by computeBreakpoints validation + return []; } const breakpoints: Coordinate[] = []; - // Calculate evenly spaced indices, avoiding start (0) and end (totalCoords-1) - // We'll distribute points evenly across the route for (let i = 0; i < count; i++) { - // Calculate position as a fraction of the route (avoiding 0 and 1) const fraction = (i + 1) / (count + 1); const index = Math.floor(fraction * totalCoords); - // Ensure index is within bounds and not at the very start or end const safeIndex = Math.max(1, Math.min(index, totalCoords - 2)); - // Convert from GeoJSON [lon, lat] to our Coordinate {lat, lon} const coordAtIndex = coordinates[safeIndex]; if (!coordAtIndex) { - continue; // Skip if coordinate is undefined + continue; } const coordinate: Coordinate = { lat: coordAtIndex[1], @@ -101,7 +84,6 @@ function extractBreakpoints( breakpoints.push(coordinate); usedCoordinates.push(coordinate); } else { - // If duplicate, try nearby indices let offset = 1; let found = false; while (!found && offset < 10) { @@ -110,7 +92,7 @@ function extractBreakpoints( if (altIndex > 0 && altIndex < totalCoords - 1) { const altCoordAtIndex = coordinates[altIndex]; if (!altCoordAtIndex) { - continue; // Skip if coordinate is undefined + continue; } const altCoordinate: Coordinate = { lat: altCoordAtIndex[1], @@ -132,10 +114,9 @@ function extractBreakpoints( offset++; } - // If still not found after trying nearby indices, skip this point to guarantee uniqueness if (!found) { console.warn( - `[Route ${routeLabel}] Skipping duplicate breakpoint at index ${safeIndex} (${coordinate.lat}, ${coordinate.lon}) because no unique alternative was found nearby.` + `[Route ${routeLabel}] Skipping duplicate breakpoint at index ${safeIndex} (${coordinate.lat}, ${coordinate.lon})` ); } } @@ -144,24 +125,7 @@ function extractBreakpoints( return breakpoints; } -/** - * Compute breakpoints for multiple routes - * - * @param routes - Array of route objects (max 3 routes) - * @returns Array of route breakpoints with unique coordinates - * - * @example - * const routes = [ - * { distance: 218.4, duration: 337.2, routeGeometry: {...} }, - * { distance: 268.5, duration: 415.2, routeGeometry: {...} } - * ]; - * const breakpoints = computeBreakpoints(routes); - * // Returns: - * // [ - * // { point_1: {lat, lon}, point_2: {lat, lon}, point_3: {lat, lon} }, - * // { point_1: {lat, lon}, point_2: {lat, lon}, point_3: {lat, lon} } - * // ] - */ +// Main entry point for computing unique breakpoints for multiple routes export function computeBreakpoints(routes: RouteInput[]): RouteBreakpoints[] { if (!routes || routes.length === 0) { throw new Error("No routes provided"); diff --git a/server/src/utils/compute/score.compute.ts b/server/src/utils/compute/score.compute.ts deleted file mode 100644 index e69de29..0000000 diff --git a/server/src/utils/compute/traffic.compute.ts b/server/src/utils/compute/traffic.compute.ts deleted file mode 100644 index e69de29..0000000 diff --git a/server/src/utils/compute/weather.compute.ts b/server/src/utils/compute/weather.compute.ts index 09c1eba..91bed2e 100644 --- a/server/src/utils/compute/weather.compute.ts +++ b/server/src/utils/compute/weather.compute.ts @@ -1,5 +1,3 @@ -// -// Type definitions for better type safety export interface Coordinate { lat: number; lon: number; @@ -15,7 +13,6 @@ export interface RoutePoints { point_7?: Coordinate; } -// Main weather data that we care about export interface WeatherMain { temp: number; feels_like: number; @@ -25,7 +22,6 @@ export interface WeatherMain { humidity: number; } -// Full weather API response (for internal use) interface WeatherAPIResponse { main?: WeatherMain; coord?: { lon: number; lat: number }; @@ -55,16 +51,13 @@ export interface RouteWeatherResult { successfulFetches: number; } -/** - * Fetches weather data for a single coordinate point - * Returns only the main weather object - */ +// Fetches current weather for a single coordinate point async function fetchWeatherForPoint( lat: number, lon: number ): Promise { const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 8000); // 8 second timeout + const timeoutId = setTimeout(() => controller.abort(), 8000); try { const response = await fetch( @@ -81,7 +74,6 @@ async function fetchWeatherForPoint( const data = (await response.json()) as WeatherAPIResponse; - // Return only the main object if (data.main) { return data.main; } @@ -100,12 +92,7 @@ async function fetchWeatherForPoint( } } -/** - * Computes weather data for multiple routes with multiple points - * - * @param routes - Array of route objects, each containing point_1, point_2, point_3, etc. - * @returns Array of weather results for each route, with only main weather data - */ +// Computes weather data for all points across multiple routes export async function computeWeather( routes: RoutePoints[] ): Promise { @@ -118,14 +105,12 @@ export async function computeWeather( throw new Error("WEATHER_API_KEY not configured"); } - // Task definition for batching interface PointTask { routeIndex: number; pointKey: string; point: Coordinate; } - // Step 1: Collect ALL points that need weather data from ALL routes const allPointTasks: PointTask[] = []; routes.forEach((route, routeIndex) => { for (let i = 1; i <= 7; i++) { @@ -137,8 +122,7 @@ export async function computeWeather( } }); - // Step 2: Execute fetches in batches (Concurrency Limiter) - // No more than N requests in parallel to avoid quota/resource issues + // Concurrency-limited batch processing const pointResultsMap = new Map(); const CONCURRENCY_LIMIT = 5; @@ -176,7 +160,7 @@ export async function computeWeather( }); } - // Step 3: Format the results back to the expected RouteWeatherResult[] structure + // Map results back to structured RouteWeatherResult array const results: RouteWeatherResult[] = routes.map((_, routeIndex) => { const pointResults = pointResultsMap.get(routeIndex) || []; return { diff --git a/server/src/utils/scheduler/computeData.scheduler.ts b/server/src/utils/scheduler/computeData.scheduler.ts index b0f33b6..0bcfe19 100644 --- a/server/src/utils/scheduler/computeData.scheduler.ts +++ b/server/src/utils/scheduler/computeData.scheduler.ts @@ -1,13 +1,3 @@ -/** - * Periodic Route Score Computation Scheduler - * - * Runs on a cron schedule and: - * 1. Fetches all saved routes from MongoDB - * 2. For each route, retrieves stored breakpoints - * 3. Fetches fresh AQI/weather data for those breakpoints - * 4. Sends data to Pathway for score computation - * 5. Updates route documents with new scores - */ import BreakPoint from "../../Schema/breakPoints.js"; import Route from "../../Schema/route.schema.js"; import { computeAQI } from "../compute/aqi.compute.js"; @@ -17,23 +7,12 @@ import { } from "../compute/weather.compute.js"; import { type PathwayRouteInput, sendToPathway } from "./pathwayClient.js"; -// ─── Configuration ──────────────────────────────────────────────────────────── const PATHWAY_URL = process.env.PATHWAY_URL || "http://localhost:8001"; const BATCH_SIZE = 5; -// Interval in milliseconds (default: 30 minutes) const SCHEDULE_INTERVAL_MS = process.env.SCHEDULE_INTERVAL_MS ? parseInt(process.env.SCHEDULE_INTERVAL_MS, 10) : 30 * 60 * 1000; -// ─── Schema note ────────────────────────────────────────────────────────────── -// routeOptionSchema stores travelMode as a plain String in MongoDB. -// When using .lean(), the value is a raw string — not an ITravelMode object. -// We cast through unknown to read it correctly. -// ───────────────────────────────────────────────────────────────────────────── - -/** - * Convert stored breakpoints to RoutePoints format expected by compute utilities - */ function breakpointsToRoutePoints( breakpoints: Array<{ pointIndex: number; @@ -45,7 +24,6 @@ function breakpointsToRoutePoints( sorted.forEach((bp, index) => { const key = `point_${index + 1}` as keyof RoutePoints; - // MongoDB stores [lon, lat] — convert to { lat, lon } routePoints[key] = { lat: bp.location.coordinates[1], lon: bp.location.coordinates[0], @@ -55,9 +33,6 @@ function breakpointsToRoutePoints( return routePoints; } -/** - * Process a single route option: fetch environmental data, compute score, persist to DB - */ async function processRoute( routeId: string, routeOptionIndex: number, @@ -75,7 +50,6 @@ async function processRoute( error?: string; }> { try { - // Step 1: Fetch breakpoints for this route option const breakpoints = await BreakPoint.find({ routeId, routeOptionIndex, @@ -90,10 +64,8 @@ async function processRoute( }; } - // Step 2: Convert breakpoints → RoutePoints const routePoints = breakpointsToRoutePoints(breakpoints); - // Step 3: Fetch Weather & AQI in parallel const [weatherResults, aqiResults] = await Promise.all([ computeWeather([routePoints]), computeAQI([routePoints]), @@ -111,8 +83,6 @@ async function processRoute( }; } - // Step 4: Build Pathway payload - // travelMode is a plain String from MongoDB lean() — pass directly const pathwayInput: PathwayRouteInput = { routeId, routeIndex: routeOptionIndex, @@ -129,7 +99,6 @@ async function processRoute( : {}), }; - // Step 5: Send to Pathway const pathwayResult = await sendToPathway(PATHWAY_URL, [pathwayInput]); if (!pathwayResult.success || !pathwayResult.routes?.[0]) { @@ -143,7 +112,6 @@ async function processRoute( const computedScore = pathwayResult.routes[0]; - // Step 6: Persist score to MongoDB await Route.updateOne( { _id: routeId }, { @@ -171,9 +139,6 @@ async function processRoute( } } -/** - * Main batch scoring job — fetches all routes and recomputes scores - */ export async function runBatchScoring(): Promise { try { const routes = await Route.find({}).lean(); @@ -182,7 +147,6 @@ export async function runBatchScoring(): Promise { return; } - // Build flat list of all route-options to process const tasks: Array<{ routeId: string; routeOptionIndex: number; @@ -196,7 +160,6 @@ export async function runBatchScoring(): Promise { for (const route of routes) { route.routes.forEach((option, index) => { - // travelMode is stored as a plain String in MongoDB (routeOptionSchema: type: String) const rawTravelMode = (option as unknown as { travelMode: string }) .travelMode; @@ -216,7 +179,6 @@ export async function runBatchScoring(): Promise { }); } - // Process in controlled batches const results: Array<{ success: boolean; routeId: string; @@ -236,40 +198,33 @@ export async function runBatchScoring(): Promise { results.push(...batchResults); - // Rate-limit friendly delay between batches if (i + BATCH_SIZE < tasks.length) { await new Promise((resolve) => setTimeout(resolve, 500)); } } } catch { - // silent — errors are captured per-route + /* batch scoring failed silently — individual route errors are captured per-task */ } } -/** - * Initialize the scheduler using setInterval - */ let _isRunning = false; export function initScheduler(): void { setInterval(async () => { if (_isRunning) { - return; // skip — previous batch still in progress + return; } _isRunning = true; try { await runBatchScoring(); } catch { - // silent + /* scheduler tick failed — _isRunning reset in finally */ } finally { _isRunning = false; } }, SCHEDULE_INTERVAL_MS); } -/** - * Run batch scoring manually (admin endpoint / startup trigger) - */ export async function runManualBatchScoring(): Promise { await runBatchScoring(); } diff --git a/server/src/utils/scheduler/pathwayClient.ts b/server/src/utils/scheduler/pathwayClient.ts index 973a250..af1c75c 100644 --- a/server/src/utils/scheduler/pathwayClient.ts +++ b/server/src/utils/scheduler/pathwayClient.ts @@ -1,11 +1,3 @@ -/** - * Pathway Client - * - * Communicates with the Django/Pathway data processing server - * to compute route health scores. - */ - -// Input format expected by Pathway endpoint export interface PathwayRouteInput { routeId?: string; routeIndex: number; @@ -44,7 +36,6 @@ export interface PathwayRouteInput { lastComputedScore?: number; } -// Output format from Pathway endpoint export interface PathwayRouteOutput { routeIndex: number; routeId?: string; @@ -107,32 +98,23 @@ export interface PathwayResponse { engine?: string; } -/** - * Send routes to Pathway for score computation - * - * @param baseUrl - Base URL of the Pathway server (e.g., "http://localhost:8001") - * @param routes - Array of route data with pre-fetched weather/AQI - * @returns Pathway response with computed scores - */ export async function sendToPathway( baseUrl: string, routes: PathwayRouteInput[] ): Promise { const url = `${baseUrl}/api/compute-scores/`; - const timeout = 30000; // 30 second timeout + const timeout = 30000; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); - console.log(`[PathwayClient] POST ${url} (timeout: ${timeout}ms)`); - try { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ routes }), + body: JSON.stringify({ routes, usePathway: false }), signal: controller.signal, }); @@ -148,7 +130,6 @@ export async function sendToPathway( } const data = (await response.json()) as PathwayResponse; - console.log(`[PathwayClient] Success from ${url}`); return data; } catch (error) { if (error instanceof Error) { @@ -177,12 +158,12 @@ export async function sendToPathway( } } -/** - * Health check for Pathway server - */ export async function checkPathwayHealth(baseUrl: string): Promise { - const url = `${baseUrl}/api/health/`; - const timeout = 30000; + const normalizedBaseUrl = baseUrl.replace(/\/+$/, ""); + const url = `${normalizedBaseUrl}/api/health/`; + const timeout = process.env.PATHWAY_TIMEOUT_MS + ? parseInt(process.env.PATHWAY_TIMEOUT_MS, 10) + : 30000; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); @@ -193,10 +174,6 @@ export async function checkPathwayHealth(baseUrl: string): Promise { signal: controller.signal, }); if (response.ok) { - const data = await response.json(); - console.log( - `[PathwayClient] Health check passed: ${JSON.stringify(data)}` - ); return true; } console.error(