From 018295d465dc0416a7dc2e37a609ac3bce9b8110 Mon Sep 17 00:00:00 2001 From: RishitGG Date: Sun, 8 Mar 2026 00:41:49 +0530 Subject: [PATCH 01/13] Add requirements.txt with all dependencies --- requirements.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6b9c98e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +pandas>=2.0.0 +numpy>=1.24.0 +scikit-learn>=1.3.0 +joblib>=1.3.0 +streamlit>=1.28.0 From 2fd60734050ec87d2886b5082014b4b6e449b5f9 Mon Sep 17 00:00:00 2001 From: Merin Theres Jose Date: Mon, 9 Mar 2026 00:00:37 +0530 Subject: [PATCH 02/13] Add React frontend, FastAPI backend, batch upload, and manual predict --- .gitignore | 50 + backend/data/__init__.py | 0 backend/data/batch_processor.py | 410 +++ backend/data/sample_data.py | 418 +++ backend/main.py | 339 ++ backend/requirements.txt | 4 + frontend/index.html | 18 + frontend/package-lock.json | 3096 ++++++++++++++++++ frontend/package.json | 29 + frontend/postcss.config.js | 6 + frontend/src/App.jsx | 25 + frontend/src/api/client.js | 47 + frontend/src/components/ConfidenceBadge.jsx | 15 + frontend/src/components/EarningsProgress.jsx | 54 + frontend/src/components/EventCard.jsx | 70 + frontend/src/components/ExplainModal.jsx | 73 + frontend/src/components/FeedbackButtons.jsx | 47 + frontend/src/components/FilterChips.jsx | 27 + frontend/src/components/Layout.jsx | 13 + frontend/src/components/SampleTripCard.jsx | 40 + frontend/src/components/Sidebar.jsx | 49 + frontend/src/components/SignalCharts.jsx | 61 + frontend/src/components/StressTips.jsx | 39 + frontend/src/components/SummaryCard.jsx | 14 + frontend/src/components/TimelineSlider.jsx | 65 + frontend/src/components/TodayTimeline.jsx | 60 + frontend/src/components/TripListItem.jsx | 77 + frontend/src/components/TripMap.jsx | 106 + frontend/src/index.css | 22 + frontend/src/main.jsx | 13 + frontend/src/pages/BatchUpload.jsx | 577 ++++ frontend/src/pages/Dashboard.jsx | 103 + frontend/src/pages/Goals.jsx | 127 + frontend/src/pages/Predict.jsx | 449 +++ frontend/src/pages/Trends.jsx | 144 + frontend/src/pages/TripDetail.jsx | 216 ++ frontend/src/pages/Trips.jsx | 221 ++ frontend/tailwind.config.js | 32 + frontend/vite.config.js | 15 + package-lock.json | 6 + 40 files changed, 7177 insertions(+) create mode 100644 .gitignore create mode 100644 backend/data/__init__.py create mode 100644 backend/data/batch_processor.py create mode 100644 backend/data/sample_data.py create mode 100644 backend/main.py create mode 100644 backend/requirements.txt create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/api/client.js create mode 100644 frontend/src/components/ConfidenceBadge.jsx create mode 100644 frontend/src/components/EarningsProgress.jsx create mode 100644 frontend/src/components/EventCard.jsx create mode 100644 frontend/src/components/ExplainModal.jsx create mode 100644 frontend/src/components/FeedbackButtons.jsx create mode 100644 frontend/src/components/FilterChips.jsx create mode 100644 frontend/src/components/Layout.jsx create mode 100644 frontend/src/components/SampleTripCard.jsx create mode 100644 frontend/src/components/Sidebar.jsx create mode 100644 frontend/src/components/SignalCharts.jsx create mode 100644 frontend/src/components/StressTips.jsx create mode 100644 frontend/src/components/SummaryCard.jsx create mode 100644 frontend/src/components/TimelineSlider.jsx create mode 100644 frontend/src/components/TodayTimeline.jsx create mode 100644 frontend/src/components/TripListItem.jsx create mode 100644 frontend/src/components/TripMap.jsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.jsx create mode 100644 frontend/src/pages/BatchUpload.jsx create mode 100644 frontend/src/pages/Dashboard.jsx create mode 100644 frontend/src/pages/Goals.jsx create mode 100644 frontend/src/pages/Predict.jsx create mode 100644 frontend/src/pages/Trends.jsx create mode 100644 frontend/src/pages/TripDetail.jsx create mode 100644 frontend/src/pages/Trips.jsx create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/vite.config.js create mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8c9237 --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# Node +node_modules/ +dist/ +.vite/ + +# Python +__pycache__/ +*.py[cod] +*.pyo +*.egg-info/ +*.egg +.eggs/ +*.whl +.venv/ +venv/ +env/ + +# Byte-compiled / optimized +*.so + +# Jupyter +.ipynb_checkpoints/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store +Thumbs.db + +# Environment +.env +.env.local +.env.*.local + +# Logs +*.log +npm-debug.log* + +# macOS +__MACOSX/ + +# Model artifacts (large binaries) +*.pkl + +# Misc +*.bak +*.tmp diff --git a/backend/data/__init__.py b/backend/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/data/batch_processor.py b/backend/data/batch_processor.py new file mode 100644 index 0000000..47a42e9 --- /dev/null +++ b/backend/data/batch_processor.py @@ -0,0 +1,410 @@ +""" +DrivePulse Backend — Batch CSV processing. +Loads trained stress + earnings models, runs inference on uploaded CSV data. +""" + +import io +import csv +import json +import math +import numpy as np +import joblib +import pandas as pd +from pathlib import Path +from typing import Optional + +ROOT = Path(__file__).resolve().parent.parent.parent # project root (Driver-Pulse/) + +# ── Stress model ────────────────────────────────────────────── + +STRESS_MODEL_DIR = ROOT / "drivepulse_stress_model" / "model" + +STRESS_SITUATIONS = { + 0: {"name": "NORMAL", "emoji": "✅", "severity": "low"}, + 1: {"name": "TRAFFIC_STOP", "emoji": "🚦", "severity": "low"}, + 2: {"name": "SPEED_BREAKER", "emoji": "⚠️", "severity": "medium"}, + 3: {"name": "CONFLICT", "emoji": "😠", "severity": "high"}, + 4: {"name": "ESCALATING", "emoji": "🔴", "severity": "high"}, + 5: {"name": "ARGUMENT_ONLY", "emoji": "🗣️", "severity": "medium"}, + 6: {"name": "MUSIC_OR_CALL", "emoji": "🎵", "severity": "low"}, +} + +NOTIFY_ON = {3, 4, 5} +SAFETY_ON = {3, 4} + +_stress_clf = None +_stress_mean = None +_stress_std = None +_stress_feats = None + + +def _load_stress_model(): + global _stress_clf, _stress_mean, _stress_std, _stress_feats + if _stress_clf is not None: + return True + try: + _stress_clf = joblib.load(STRESS_MODEL_DIR / "rf_model.pkl") + _stress_mean = np.load(STRESS_MODEL_DIR / "baseline_mean.npy") + _stress_std = np.load(STRESS_MODEL_DIR / "baseline_std.npy") + _stress_feats = json.loads( + (STRESS_MODEL_DIR / "feature_contract.json").read_text() + )["features"] + print(f"[batch] Stress model loaded ({len(_stress_feats)} features)") + return True + except Exception as e: + print(f"[batch] Stress model load failed: {e}") + return False + + +def predict_stress_row(row: dict) -> dict: + """Run stress prediction on a single row dict.""" + if not _load_stress_model(): + return _stress_fallback(row) + + x = np.array( + [float(row.get(f, 0.0)) for f in _stress_feats], dtype=np.float32 + ).reshape(1, -1) + xn = (x - _stress_mean) / _stress_std + pred = int(_stress_clf.predict(xn)[0]) + proba = _stress_clf.predict_proba(xn)[0] + conf = float(proba[pred]) + + if conf < 0.50: + pred, conf = 0, conf + + sit = STRESS_SITUATIONS[pred] + conf_label = "high" if conf >= 0.75 else ("medium" if conf >= 0.50 else "low") + + # Top 3 feature importance (simple absolute deviation approach) + deviations = [] + for i, f in enumerate(_stress_feats): + val = float(row.get(f, 0.0)) + mean_val = float(_stress_mean[i]) if i < len(_stress_mean) else 0 + std_val = float(_stress_std[i]) if i < len(_stress_std) else 1 + z = abs((val - mean_val) / max(std_val, 1e-6)) + deviations.append({"feature": f, "z_score": round(z, 3), "value": round(val, 3)}) + deviations.sort(key=lambda d: d["z_score"], reverse=True) + top3 = deviations[:3] + + return { + "situation_id": pred, + "situation_name": sit["name"], + "emoji": sit["emoji"], + "severity": sit["severity"], + "confidence": round(conf, 3), + "confidence_level": conf_label, + "should_notify": pred in NOTIFY_ON and conf_label != "low", + "is_safety_critical": pred in SAFETY_ON and conf_label == "high", + "top_features": top3, + "all_probabilities": { + STRESS_SITUATIONS[i]["name"]: round(float(proba[i]), 4) + for i in range(len(proba)) + }, + } + + +def _stress_fallback(row: dict) -> dict: + """Rule-based fallback when model files are missing.""" + motion = float(row.get("motion_p95", 0)) + audio = float(row.get("audio_db_p90", 50)) + speed = float(row.get("speed_mean", 30)) + lead = float(row.get("audio_leads_motion", 0)) + + if motion > 3.5 and audio > 80: + pred, conf = (4, 0.60) if lead < -5 else (3, 0.65) + elif motion > 3.5 and speed < 20: + pred, conf = 2, 0.70 + elif motion > 3.5: + pred, conf = 1, 0.65 + elif audio > 80: + pred, conf = 5, 0.60 + else: + pred, conf = 0, 0.90 + + sit = STRESS_SITUATIONS[pred] + return { + "situation_id": pred, + "situation_name": sit["name"], + "emoji": sit["emoji"], + "severity": sit["severity"], + "confidence": round(conf, 3), + "confidence_level": "medium", + "should_notify": pred in NOTIFY_ON, + "is_safety_critical": pred in SAFETY_ON, + "top_features": [], + "all_probabilities": {}, + } + + +# ── Earnings model ──────────────────────────────────────────── + +EARNINGS_MODEL_DIR = ROOT / "earnings" / "earnings" / "model" + +EARNINGS_FEATURES = [ + "elapsed_hours", "current_velocity", "velocity_delta", + "trips_completed", "trip_rate", "hour_of_day", + "is_morning_rush", "is_lunch_rush", + "velocity_last_1", "velocity_last_2", "velocity_last_3", + "rolling_velocity_3", "rolling_velocity_5", "goal_pressure", +] + +_earnings_clf = None + + +def _load_earnings_model(): + global _earnings_clf + if _earnings_clf is not None: + return True + try: + _earnings_clf = joblib.load(EARNINGS_MODEL_DIR / "rf_model.pkl") + print(f"[batch] Earnings model loaded") + return True + except Exception as e: + print(f"[batch] Earnings model load failed: {e}") + return False + + +def predict_earnings_row(row: dict) -> dict: + """Run earnings prediction on a single row dict.""" + if not _load_earnings_model(): + return {"predicted_velocity": None, "error": "Model not loaded"} + + x = np.array( + [float(row.get(f, 0.0)) for f in EARNINGS_FEATURES], dtype=np.float32 + ).reshape(1, -1) + predicted = float(_earnings_clf.predict(x)[0]) + predicted = max(0, round(predicted, 2)) + + target_velocity = float(row.get("target_velocity", 200)) + current_velocity = float(row.get("current_velocity", 0)) + target_earnings = float(row.get("target_earnings", 1800)) + current_earnings = float(row.get("cumulative_earnings", 0)) + elapsed = float(row.get("elapsed_hours", 0)) + remaining_earnings = max(0, target_earnings - current_earnings) + + if predicted > 0: + hours_needed = remaining_earnings / predicted + else: + hours_needed = None + + if predicted >= target_velocity * 1.1: + forecast = "ahead" + elif predicted >= target_velocity * 0.9: + forecast = "on_track" + else: + forecast = "at_risk" + + return { + "predicted_velocity": round(predicted, 2), + "target_velocity": target_velocity, + "current_velocity": current_velocity, + "forecast_status": forecast, + "remaining_earnings": round(remaining_earnings, 2), + "hours_to_target": round(hours_needed, 2) if hours_needed else None, + "pct_target": round(min(100, (current_earnings / max(target_earnings, 1)) * 100), 1), + } + + +# ── Feature engineering for earnings (from raw trip CSV) ────── + +def engineer_earnings_features(df: pd.DataFrame) -> pd.DataFrame: + """ + Takes a raw trip-level DataFrame and engineers the 14 features + needed for the earnings model. Columns expected: + driver_id, timestamp, cumulative_earnings, elapsed_hours, + current_velocity, target_velocity, velocity_delta, + trips_completed, target_earnings + """ + df = df.copy() + + # Parse timestamp → hour info + if "timestamp" in df.columns: + ts = pd.to_datetime(df["timestamp"], errors="coerce") + df["hour_of_day"] = ts.dt.hour.fillna(12).astype(int) + elif "hour_of_day" not in df.columns: + df["hour_of_day"] = 12 + + df["is_morning_rush"] = df["hour_of_day"].between(7, 9).astype(int) + df["is_lunch_rush"] = df["hour_of_day"].between(12, 14).astype(int) + + # trip_rate + df["elapsed_hours"] = pd.to_numeric(df.get("elapsed_hours", 1), errors="coerce").fillna(1) + df["trips_completed"] = pd.to_numeric(df.get("trips_completed", 0), errors="coerce").fillna(0) + df["trip_rate"] = df["trips_completed"] / df["elapsed_hours"].replace(0, 1) + + # Velocity lags + rolling + df["current_velocity"] = pd.to_numeric(df.get("current_velocity", 0), errors="coerce").fillna(0).clip(0, 600) + df["velocity_delta"] = pd.to_numeric(df.get("velocity_delta", 0), errors="coerce").fillna(0) + df["target_velocity"] = pd.to_numeric(df.get("target_velocity", 200), errors="coerce").fillna(200) + + df = df.sort_values(["driver_id", "hour_of_day"] if "driver_id" in df.columns else ["hour_of_day"]) + grp = "driver_id" if "driver_id" in df.columns else None + + if grp: + df["velocity_last_1"] = df.groupby(grp)["current_velocity"].shift(1) + df["velocity_last_2"] = df.groupby(grp)["current_velocity"].shift(2) + df["velocity_last_3"] = df.groupby(grp)["current_velocity"].shift(3) + df["rolling_velocity_3"] = df.groupby(grp)["current_velocity"].transform( + lambda s: s.rolling(3, min_periods=1).mean() + ) + df["rolling_velocity_5"] = df.groupby(grp)["current_velocity"].transform( + lambda s: s.rolling(5, min_periods=1).mean() + ) + else: + df["velocity_last_1"] = df["current_velocity"].shift(1) + df["velocity_last_2"] = df["current_velocity"].shift(2) + df["velocity_last_3"] = df["current_velocity"].shift(3) + df["rolling_velocity_3"] = df["current_velocity"].rolling(3, min_periods=1).mean() + df["rolling_velocity_5"] = df["current_velocity"].rolling(5, min_periods=1).mean() + + df["goal_pressure"] = df["target_velocity"] - df["current_velocity"] + + # Fill NaNs from shifting + df = df.bfill().ffill().fillna(0) + + return df + + +# ── Batch processing ────────────────────────────────────────── + +def process_stress_csv(csv_content: str) -> dict: + """Process a CSV of sensor windows. Returns per-row predictions + summary.""" + _load_stress_model() + + reader = csv.DictReader(io.StringIO(csv_content)) + rows = list(reader) + + if not rows: + return {"error": "CSV is empty", "results": [], "summary": {}} + + results = [] + severity_counts = {"low": 0, "medium": 0, "high": 0} + situation_counts = {} + + for i, row in enumerate(rows): + pred = predict_stress_row(row) + pred["row_index"] = i + # Carry through any extra columns (trip_id, timestamp, etc.) + for k in ("trip_id", "window_id", "timestamp", "start_time", "driver_id"): + if k in row: + pred[k] = row[k] + + results.append(pred) + severity_counts[pred["severity"]] = severity_counts.get(pred["severity"], 0) + 1 + situation_counts[pred["situation_name"]] = situation_counts.get(pred["situation_name"], 0) + 1 + + total = len(results) + avg_confidence = round(sum(r["confidence"] for r in results) / total, 3) + notify_count = sum(1 for r in results if r["should_notify"]) + safety_count = sum(1 for r in results if r["is_safety_critical"]) + + return { + "results": results, + "summary": { + "total_windows": total, + "severity_counts": severity_counts, + "situation_counts": situation_counts, + "avg_confidence": avg_confidence, + "notifications_triggered": notify_count, + "safety_critical_count": safety_count, + "stress_score": round( + (severity_counts.get("high", 0) * 5 + severity_counts.get("medium", 0) * 3) + / max(total, 1), + 2, + ), + }, + } + + +def process_earnings_csv(csv_content: str) -> dict: + """Process a CSV of earnings/trip data. Returns per-row velocity predictions + summary.""" + _load_earnings_model() + + df = pd.read_csv(io.StringIO(csv_content)) + + if df.empty: + return {"error": "CSV is empty", "results": [], "summary": {}} + + # Fill defaults for missing columns + for col, default in [ + ("driver_id", "driver-001"), + ("target_velocity", 200), + ("velocity_delta", 0), + ("target_earnings", 1800), + ("cumulative_earnings", 0), + ]: + if col not in df.columns: + df[col] = default + + df = engineer_earnings_features(df) + + results = [] + total_predicted = 0 + + for i, row in df.iterrows(): + row_dict = row.to_dict() + pred = predict_earnings_row(row_dict) + pred["row_index"] = int(i) + for k in ("driver_id", "timestamp", "trip_id", "hour_of_day"): + if k in row_dict: + pred[k] = row_dict[k] + if isinstance(pred[k], float) and not math.isnan(pred[k]): + pred[k] = int(pred[k]) if k == "hour_of_day" else pred[k] + elif isinstance(pred[k], float) and math.isnan(pred[k]): + pred[k] = None + + pred["cumulative_earnings"] = round(float(row_dict.get("cumulative_earnings", 0)), 2) + pred["elapsed_hours"] = round(float(row_dict.get("elapsed_hours", 0)), 2) + + results.append(pred) + if pred["predicted_velocity"] is not None: + total_predicted += pred["predicted_velocity"] + + total = len(results) + avg_velocity = round(total_predicted / max(total, 1), 2) + + forecast_counts = {"ahead": 0, "on_track": 0, "at_risk": 0} + for r in results: + forecast_counts[r.get("forecast_status", "on_track")] += 1 + + return { + "results": results, + "summary": { + "total_entries": total, + "avg_predicted_velocity": avg_velocity, + "forecast_counts": forecast_counts, + "best_velocity": max((r["predicted_velocity"] for r in results if r["predicted_velocity"]), default=0), + "worst_velocity": min((r["predicted_velocity"] for r in results if r["predicted_velocity"]), default=0), + }, + } + + +# ── Template generators ────────────────────────────────────── + +def stress_csv_template() -> str: + """Return a CSV template string with headers + 2 sample rows.""" + feats = [ + "motion_max", "motion_mean", "motion_p95", "motion_std", + "brake_intensity", "lateral_max", "z_dev_max", + "speed_mean", "speed_at_brake", "speed_drop", + "spikes_above3", "spikes_above5", + "audio_db_max", "audio_db_mean", "audio_db_p90", "audio_db_std", + "audio_class_max", "audio_class_mean", "sustained_max", "sustained_sum", + "cadence_var_mean", "cadence_var_max", + "argument_frac", "loud_frac", + "audio_leads_motion", "audio_onset_sec", "brake_t_sec", + "is_low_speed", "both_elevated", "audio_only", + ] + header = "trip_id,timestamp," + ",".join(feats) + row1 = "trip-001,08:15:00,1.2,0.6,1.1,0.3,0.8,0.5,0.2,35,35,5,0,0,68,62,67,4,2,1.2,0,0,0.1,0.15,0.0,0.05,0,15,15,0,0,0" + row2 = "trip-002,09:30:00,5.1,1.8,4.8,1.4,4.9,2.1,0.4,40,40,25,4,2,94,82,91,8,5,3.8,45,180,0.72,0.95,0.55,0.82,-1.5,12,14,0,1,0" + return header + "\n" + row1 + "\n" + row2 + "\n" + + +def earnings_csv_template() -> str: + """Return a CSV template string for earnings upload.""" + header = "driver_id,timestamp,cumulative_earnings,elapsed_hours,current_velocity,target_velocity,velocity_delta,trips_completed,target_earnings" + row1 = "driver-001,08:00:00,0,0.5,0,200,0,0,1800" + row2 = "driver-001,09:00:00,185,1.5,185,200,-15,2,1800" + row3 = "driver-001,10:00:00,420,2.5,210,200,10,4,1800" + return header + "\n" + row1 + "\n" + row2 + "\n" + row3 + "\n" diff --git a/backend/data/sample_data.py b/backend/data/sample_data.py new file mode 100644 index 0000000..dd19539 --- /dev/null +++ b/backend/data/sample_data.py @@ -0,0 +1,418 @@ +""" +DrivePulse Backend — Sample data generator. +Produces realistic trips, events, signals, and driver metrics. +""" + +import random +import math +import uuid +from datetime import datetime, timedelta +from typing import List, Dict, Any + +random.seed(42) + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +SITUATIONS = { + 0: {"name": "NORMAL", "emoji": "✅", "severity": "low"}, + 1: {"name": "TRAFFIC_STOP", "emoji": "🚦", "severity": "low"}, + 2: {"name": "SPEED_BREAKER", "emoji": "⚠️", "severity": "medium"}, + 3: {"name": "CONFLICT", "emoji": "😠", "severity": "high"}, + 4: {"name": "ESCALATING", "emoji": "🔴", "severity": "high"}, + 5: {"name": "ARGUMENT_ONLY", "emoji": "🗣️", "severity": "medium"}, + 6: {"name": "MUSIC_OR_CALL", "emoji": "🎵", "severity": "low"}, +} + +STRESS_TIPS = [ + {"id": 1, "title": "Deep Breathing", "text": "Try 4-7-8 breathing: inhale 4s, hold 7s, exhale 8s. Helps reduce cortisol instantly.", "cta": "Try now"}, + {"id": 2, "title": "Pull Over Briefly", "text": "If safe, stop for 2 minutes. A short pause resets your stress response.", "cta": "Find safe spot"}, + {"id": 3, "title": "Adjust Temperature", "text": "Lower the AC by 2°. Cooler air helps reduce tension and improve alertness.", "cta": "Got it"}, + {"id": 4, "title": "Play Calming Audio", "text": "Switch to lo-fi or nature sounds. Reduces heart rate within 3 minutes.", "cta": "Open music"}, + {"id": 5, "title": "Stretch at Next Stop", "text": "Roll your shoulders and stretch your neck at the next red light or stop.", "cta": "Remind me"}, + {"id": 6, "title": "Hydrate", "text": "Dehydration increases stress hormones. Take a sip of water.", "cta": "Thanks"}, + {"id": 7, "title": "Positive Self-Talk", "text": "Remind yourself: 'This moment will pass. I'm doing well today.'", "cta": "Noted"}, + {"id": 8, "title": "Micro-break", "text": "After this trip, take a 5-minute walk. It clears mental fatigue.", "cta": "Schedule break"}, +] + +# Bangalore area coordinates for route generation +BANGALORE_CENTER = (12.9716, 77.5946) +ROUTE_ANCHORS = [ + [(12.9716, 77.5946), (12.9780, 77.6050), (12.9850, 77.6150), (12.9900, 77.6200)], + [(12.9350, 77.6140), (12.9400, 77.6050), (12.9500, 77.5970), (12.9600, 77.5900)], + [(12.9600, 77.5700), (12.9550, 77.5800), (12.9500, 77.5900), (12.9450, 77.6000)], + [(12.9800, 77.5500), (12.9750, 77.5600), (12.9700, 77.5750), (12.9716, 77.5946)], + [(12.9200, 77.6200), (12.9300, 77.6100), (12.9400, 77.5950), (12.9500, 77.5850)], + [(12.9900, 77.5800), (12.9850, 77.5900), (12.9800, 77.6000), (12.9750, 77.6100)], +] + + +def _interp_route(anchors: list, n_points: int = 60) -> list: + """Interpolate between anchor points to create a smooth route.""" + route = [] + segs = len(anchors) - 1 + pts_per_seg = max(n_points // segs, 2) + for i in range(segs): + for j in range(pts_per_seg): + t = j / pts_per_seg + lat = anchors[i][0] + t * (anchors[i + 1][0] - anchors[i][0]) + random.gauss(0, 0.0002) + lng = anchors[i][1] + t * (anchors[i + 1][1] - anchors[i][1]) + random.gauss(0, 0.0002) + route.append([round(lat, 6), round(lng, 6)]) + route.append(list(anchors[-1])) + return route + + +def _gen_signals(duration_min: int, events: list) -> Dict[str, list]: + """Generate time-series signals: speed, accel_magnitude, audio_db.""" + n = duration_min * 6 # one sample every 10s + ts = list(range(0, duration_min * 60, 10)) + speed = [] + accel = [] + audio = [] + + base_speed = random.uniform(20, 45) + for i in range(n): + t_sec = ts[i] if i < len(ts) else i * 10 + # Base speed with traffic variation + s = base_speed + 10 * math.sin(2 * math.pi * i / n) + random.gauss(0, 3) + a = abs(random.gauss(1.0, 0.3)) + d = random.gauss(55, 5) + + # Inject spikes near events + for ev in events: + ev_sec = ev["offset_sec"] + if abs(t_sec - ev_sec) < 30: + proximity = 1 - abs(t_sec - ev_sec) / 30 + if ev["label"] in ("CONFLICT", "ESCALATING", "ARGUMENT_ONLY"): + d += 20 * proximity + a += 1.5 * proximity + elif ev["label"] == "SPEED_BREAKER": + a += 3 * proximity + s = max(5, s - 15 * proximity) + elif ev["label"] == "TRAFFIC_STOP": + s = max(0, s - s * proximity) + + speed.append(round(max(0, s), 1)) + accel.append(round(max(0, a), 2)) + audio.append(round(max(30, min(100, d)), 1)) + + return {"timestamps": ts[:n], "speed": speed, "accel_magnitude": accel, "audio_db": audio} + + +def _gen_events(duration_min: int, stress_level: str) -> list: + """Generate random events for a trip based on stress level.""" + if stress_level == "low": + n_events = random.randint(0, 2) + pool = [0, 1, 6] + elif stress_level == "medium": + n_events = random.randint(2, 4) + pool = [0, 1, 2, 5, 6] + else: + n_events = random.randint(3, 6) + pool = [1, 2, 3, 4, 5] + + events = [] + used_offsets = set() + for _ in range(n_events): + sit_id = random.choice(pool) + sit = SITUATIONS[sit_id] + offset = random.randint(60, (duration_min - 1) * 60) + while any(abs(offset - u) < 30 for u in used_offsets): + offset = random.randint(60, (duration_min - 1) * 60) + used_offsets.add(offset) + + confidence = round(random.uniform(0.55, 0.98), 2) + conf_level = "high" if confidence > 0.85 else ("medium" if confidence > 0.65 else "low") + + # Feature contributions (SHAP-like) + all_contribs = [ + {"feature": "accel_magnitude", "direction": "↑", "contribution": round(random.uniform(0.05, 0.35), 3)}, + {"feature": "audio_db_max", "direction": "↑", "contribution": round(random.uniform(0.03, 0.30), 3)}, + {"feature": "speed_drop", "direction": "↑", "contribution": round(random.uniform(0.02, 0.25), 3)}, + {"feature": "brake_intensity", "direction": "↑", "contribution": round(random.uniform(0.01, 0.20), 3)}, + {"feature": "lateral_max", "direction": "↑", "contribution": round(random.uniform(0.01, 0.15), 3)}, + {"feature": "sustained_max", "direction": "↑" if sit_id in (3, 4, 5) else "→", "contribution": round(random.uniform(0.01, 0.18), 3)}, + {"feature": "cadence_var_mean", "direction": "↑" if sit_id in (3, 4) else "→", "contribution": round(random.uniform(0.005, 0.10), 3)}, + ] + all_contribs.sort(key=lambda x: x["contribution"], reverse=True) + top3 = all_contribs[:3] + + model_inputs = { + "motion_max": round(random.uniform(1, 8), 2), + "audio_db_max": round(random.uniform(50, 95), 1), + "speed_mean": round(random.uniform(5, 50), 1), + "brake_intensity": round(random.uniform(0, 5), 2), + "lateral_max": round(random.uniform(0.2, 3), 2), + } + + events.append({ + "id": str(uuid.uuid4())[:8], + "offset_sec": offset, + "timestamp": None, # filled later + "label": sit["name"], + "emoji": sit["emoji"], + "severity": sit["severity"], + "situation_id": sit_id, + "confidence": confidence, + "confidence_level": conf_level, + "explain": { + "model_inputs": model_inputs, + "top_features": top3, + "summary": f"Detected {sit['name'].lower().replace('_', ' ')} — primary driver: {top3[0]['feature']} {top3[0]['direction']}" + }, + "feedback": None, + }) + + events.sort(key=lambda e: e["offset_sec"]) + return events + + +def _gen_trip(trip_idx: int, day: datetime, hour: int) -> Dict[str, Any]: + """Generate a single trip.""" + stress_options = ["low", "low", "medium", "medium", "high"] + stress_level = random.choice(stress_options) + + duration_min = random.randint(12, 55) + start_time = day.replace(hour=hour, minute=random.randint(0, 59), second=0) + end_time = start_time + timedelta(minutes=duration_min) + + route_anchors = random.choice(ROUTE_ANCHORS) + route = _interp_route(route_anchors, n_points=max(30, duration_min)) + + distance_km = round(random.uniform(3, 25), 1) + base_fare = 30 + distance_km * 12 + duration_min * 2 + surge = round(random.choice([1.0, 1.0, 1.0, 1.2, 1.5, 1.8, 2.0]), 1) + fare = round(base_fare * surge, 0) + + events = _gen_events(duration_min, stress_level) + stress_score = 0 + for ev in events: + w = {"low": 1, "medium": 3, "high": 5}.get(ev["severity"], 1) + stress_score += w * ev["confidence"] + stress_score = round(min(10, stress_score), 1) + + # Fill event timestamps + for ev in events: + ev["timestamp"] = (start_time + timedelta(seconds=ev["offset_sec"])).isoformat() + + signals = _gen_signals(duration_min, events) + + # Place event markers on route + for ev in events: + frac = ev["offset_sec"] / (duration_min * 60) + idx = min(int(frac * len(route)), len(route) - 1) + ev["location"] = route[idx] + + trip_id = f"trip-{trip_idx:03d}" + return { + "id": trip_id, + "date": day.strftime("%Y-%m-%d"), + "start_time": start_time.isoformat(), + "end_time": end_time.isoformat(), + "duration_min": duration_min, + "distance_km": distance_km, + "fare": fare, + "surge_multiplier": surge, + "stress_score": stress_score, + "stress_level": stress_level, + "events_count": len(events), + "events": events, + "route": route, + "pickup": route[0], + "dropoff": route[-1], + "signals": signals, + } + + +# --------------------------------------------------------------------------- +# Public generators +# --------------------------------------------------------------------------- + +def generate_trips(today: datetime = None) -> List[Dict]: + """Generate ~8-10 trips for today and ~8-10 for yesterday.""" + if today is None: + today = datetime(2026, 3, 8) + yesterday = today - timedelta(days=1) + + trips = [] + idx = 1 + + for day, label in [(today, "today"), (yesterday, "yesterday")]: + n_trips = random.randint(7, 10) + hours = sorted(random.sample(range(6, 23), n_trips)) + for h in hours: + trips.append(_gen_trip(idx, day, h)) + idx += 1 + + return trips + + +def generate_driver_profile() -> Dict: + return { + "id": "driver-001", + "name": "Ravi Kumar", + "city": "Bangalore", + "rating": 4.82, + "experience_months": 18, + "avg_hours_per_day": 10, + "avg_earnings_per_hour": 185, + "shift_preference": "morning", + } + + +def generate_goals() -> Dict: + return { + "daily_target": 1800, + "current_earnings": 1230, + "target_hours": 10, + "current_hours": 6.5, + "trips_completed": 8, + "forecast_status": "on_track", + "goal_probability": 0.78, + "required_velocity": 220, + "current_velocity": 189, + } + + +def build_dashboard(trips: List[Dict], goals: Dict) -> Dict: + """Build dashboard summary from trips and goals.""" + today_str = datetime(2026, 3, 8).strftime("%Y-%m-%d") + today_trips = [t for t in trips if t["date"] == today_str] + + total_earnings = sum(t["fare"] for t in today_trips) + total_hours = round(sum(t["duration_min"] for t in today_trips) / 60, 1) + stress_events = sum(1 for t in today_trips for e in t["events"] if e["severity"] in ("medium", "high")) + high_stress_count = sum(1 for t in today_trips for e in t["events"] if e["severity"] == "high") + pct_target = round(min(100, (total_earnings / goals["daily_target"]) * 100), 1) if goals["daily_target"] else 0 + + return { + "date": today_str, + "total_trips": len(today_trips), + "total_hours": total_hours, + "total_earnings": total_earnings, + "stress_events": stress_events, + "high_stress_events": high_stress_count, + "pct_target_achieved": pct_target, + "avg_stress_score": round(sum(t["stress_score"] for t in today_trips) / max(len(today_trips), 1), 1), + "earnings_velocity": goals["current_velocity"], + } + + +def build_weekly_metrics(trips: List[Dict]) -> Dict: + """Build aggregated metrics for trends page.""" + today = datetime(2026, 3, 8) + days = [] + for i in range(6, -1, -1): + day = today - timedelta(days=i) + day_str = day.strftime("%Y-%m-%d") + day_label = day.strftime("%a") + day_trips = [t for t in trips if t["date"] == day_str] + + # For days without data, simulate + if not day_trips: + n = random.randint(5, 10) + earnings = round(random.uniform(1200, 2200), 0) + hours = round(random.uniform(6, 11), 1) + stress = round(random.uniform(2, 6), 1) + stress_events = random.randint(2, 8) + else: + n = len(day_trips) + earnings = sum(t["fare"] for t in day_trips) + hours = round(sum(t["duration_min"] for t in day_trips) / 60, 1) + stress = round(sum(t["stress_score"] for t in day_trips) / n, 1) + stress_events = sum(1 for t in day_trips for e in t["events"] if e["severity"] in ("medium", "high")) + + days.append({ + "date": day_str, + "day": day_label, + "trips": n, + "earnings": earnings, + "hours": hours, + "avg_stress": stress, + "stress_events": stress_events, + "velocity": round(earnings / max(hours, 1), 0), + }) + + return { + "range": "7d", + "days": days, + "summary": { + "avg_daily_earnings": round(sum(d["earnings"] for d in days) / 7, 0), + "avg_daily_trips": round(sum(d["trips"] for d in days) / 7, 1), + "avg_stress": round(sum(d["avg_stress"] for d in days) / 7, 1), + "total_earnings": sum(d["earnings"] for d in days), + "total_trips": sum(d["trips"] for d in days), + "best_day": max(days, key=lambda d: d["earnings"])["day"], + }, + } + + +def build_monthly_metrics() -> Dict: + """Build 30-day metric trend.""" + today = datetime(2026, 3, 8) + days = [] + for i in range(29, -1, -1): + day = today - timedelta(days=i) + n = random.randint(5, 12) + earnings = round(random.uniform(1000, 2500), 0) + hours = round(random.uniform(5, 12), 1) + days.append({ + "date": day.strftime("%Y-%m-%d"), + "day": day.strftime("%d %b"), + "trips": n, + "earnings": earnings, + "hours": hours, + "avg_stress": round(random.uniform(1.5, 7), 1), + "stress_events": random.randint(0, 10), + "velocity": round(earnings / max(hours, 1), 0), + }) + return { + "range": "30d", + "days": days, + "summary": { + "avg_daily_earnings": round(sum(d["earnings"] for d in days) / 30, 0), + "avg_daily_trips": round(sum(d["trips"] for d in days) / 30, 1), + "avg_stress": round(sum(d["avg_stress"] for d in days) / 30, 1), + "total_earnings": sum(d["earnings"] for d in days), + "total_trips": sum(d["trips"] for d in days), + }, + } + + +# --------------------------------------------------------------------------- +# Singleton data store +# --------------------------------------------------------------------------- + +_TRIPS = None +_PROFILE = None +_GOALS = None + + +def get_trips(): + global _TRIPS + if _TRIPS is None: + _TRIPS = generate_trips() + return _TRIPS + + +def get_profile(): + global _PROFILE + if _PROFILE is None: + _PROFILE = generate_driver_profile() + return _PROFILE + + +def get_goals(): + global _GOALS + if _GOALS is None: + _GOALS = generate_goals() + return _GOALS + + +def set_goal_target(target: float): + global _GOALS + goals = get_goals() + goals["daily_target"] = target + _GOALS = goals + return _GOALS diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..e7de4d2 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,339 @@ +""" +DrivePulse Backend — FastAPI application. +Serves trip data, events, metrics, goals, and stress tips. +""" + +from fastapi import FastAPI, HTTPException, Query, UploadFile, File +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import PlainTextResponse +from pydantic import BaseModel +from typing import Optional, List +from datetime import datetime + +from data.sample_data import ( + get_trips, get_profile, get_goals, set_goal_target, + build_dashboard, build_weekly_metrics, build_monthly_metrics, + STRESS_TIPS, SITUATIONS, +) +from data.batch_processor import ( + process_stress_csv, process_earnings_csv, + stress_csv_template, earnings_csv_template, + predict_stress_row, predict_earnings_row, +) + +app = FastAPI(title="DrivePulse API", version="1.0.0") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# ── Feedback storage (in-memory) ────────────────────────────────────────── + +_feedback_store: dict = {} + + +class FeedbackPayload(BaseModel): + label: str # "correct" | "incorrect" | "not_relevant" + comment: Optional[str] = None + + +class GoalPayload(BaseModel): + daily_target: float + + +# ── Routes ──────────────────────────────────────────────────────────────── + +@app.get("/api/health") +def health(): + return {"status": "ok", "timestamp": datetime.now().isoformat()} + + +@app.get("/api/profile") +def profile(): + return get_profile() + + +# ── Dashboard ───────────────────────────────────────────────────────────── + +@app.get("/api/dashboard") +def dashboard(): + trips = get_trips() + goals = get_goals() + return build_dashboard(trips, goals) + + +# ── Trips ───────────────────────────────────────────────────────────────── + +@app.get("/api/trips") +def list_trips( + date: Optional[str] = Query(None, description="YYYY-MM-DD"), + stress: Optional[str] = Query(None, description="high | medium | low | any"), + earnings_min: Optional[float] = Query(None), + earnings_max: Optional[float] = Query(None), + time_of_day: Optional[str] = Query(None, description="morning|afternoon|evening|night"), + duration_min: Optional[int] = Query(None), + duration_max: Optional[int] = Query(None), + preset: Optional[str] = Query(None, description="high_stress|high_earnings|night|short"), +): + trips = get_trips() + + # Presets + if preset == "high_stress": + stress = "high" + elif preset == "high_earnings": + earnings_min = 500 + elif preset == "night": + time_of_day = "night" + elif preset == "short": + duration_max = 15 + + if date: + trips = [t for t in trips if t["date"] == date] + if stress and stress != "any": + trips = [t for t in trips if t["stress_level"] == stress] + if earnings_min is not None: + trips = [t for t in trips if t["fare"] >= earnings_min] + if earnings_max is not None: + trips = [t for t in trips if t["fare"] <= earnings_max] + if duration_min is not None: + trips = [t for t in trips if t["duration_min"] >= duration_min] + if duration_max is not None: + trips = [t for t in trips if t["duration_min"] <= duration_max] + if time_of_day: + def _in_tod(t): + h = datetime.fromisoformat(t["start_time"]).hour + if time_of_day == "morning": + return 5 <= h < 12 + elif time_of_day == "afternoon": + return 12 <= h < 17 + elif time_of_day == "evening": + return 17 <= h < 21 + elif time_of_day == "night": + return h >= 21 or h < 5 + return True + trips = [t for t in trips if _in_tod(t)] + + # Strip heavy data from list view + lite = [] + for t in trips: + lite.append({k: v for k, v in t.items() if k not in ("signals", "route", "events")}) + lite[-1]["events_summary"] = [ + {"label": e["label"], "severity": e["severity"]} + for e in t["events"] + ] + return {"trips": lite, "count": len(lite)} + + +@app.get("/api/trips/{trip_id}") +def get_trip(trip_id: str): + trips = get_trips() + trip = next((t for t in trips if t["id"] == trip_id), None) + if not trip: + raise HTTPException(404, "Trip not found") + # Attach feedback if any + for ev in trip["events"]: + ev["feedback"] = _feedback_store.get(ev["id"]) + return trip + + +@app.get("/api/sample-trip") +def sample_trip(): + """Return a feature-rich sample trip for judges / quick start.""" + trips = get_trips() + # Pick the trip with the most events + best = max(trips, key=lambda t: len(t["events"])) + return best + + +# ── Events & Feedback ───────────────────────────────────────────────────── + +@app.get("/api/trips/{trip_id}/events") +def trip_events(trip_id: str): + trips = get_trips() + trip = next((t for t in trips if t["id"] == trip_id), None) + if not trip: + raise HTTPException(404, "Trip not found") + events = trip["events"] + for ev in events: + ev["feedback"] = _feedback_store.get(ev["id"]) + return {"trip_id": trip_id, "events": events} + + +@app.post("/api/events/{event_id}/feedback") +def post_feedback(event_id: str, payload: FeedbackPayload): + _feedback_store[event_id] = {"label": payload.label, "comment": payload.comment} + return {"status": "ok", "event_id": event_id, "feedback": _feedback_store[event_id]} + + +# ── Goals ───────────────────────────────────────────────────────────────── + +@app.get("/api/goals") +def goals(): + return get_goals() + + +@app.post("/api/goals") +def update_goal(payload: GoalPayload): + return set_goal_target(payload.daily_target) + + +# ── Metrics / Trends ────────────────────────────────────────────────────── + +@app.get("/api/metrics") +def metrics(range: str = Query("7d", description="7d | 30d")): + if range == "30d": + return build_monthly_metrics() + return build_weekly_metrics(get_trips()) + + +# ── Stress Tips ─────────────────────────────────────────────────────────── + +@app.get("/api/tips") +def tips(severity: Optional[str] = Query(None)): + import random as _r + selected = _r.sample(STRESS_TIPS, min(3, len(STRESS_TIPS))) + return {"tips": selected} + + +# ── Situations reference ────────────────────────────────────────────────── + +@app.get("/api/situations") +def situations(): + return SITUATIONS + + +# ── Batch CSV Upload ────────────────────────────────────────────────────── + +@app.post("/api/batch/stress") +async def batch_stress(file: UploadFile = File(...)): + """Upload a CSV of sensor windows → get stress predictions for all rows.""" + content = (await file.read()).decode("utf-8") + if not content.strip(): + raise HTTPException(400, "Empty file") + result = process_stress_csv(content) + if "error" in result and result["error"]: + raise HTTPException(400, result["error"]) + return result + + +@app.post("/api/batch/earnings") +async def batch_earnings(file: UploadFile = File(...)): + """Upload a CSV of trip/earnings data → get velocity predictions for all rows.""" + content = (await file.read()).decode("utf-8") + if not content.strip(): + raise HTTPException(400, "Empty file") + result = process_earnings_csv(content) + if "error" in result and result["error"]: + raise HTTPException(400, result["error"]) + return result + + +@app.get("/api/batch/template/stress", response_class=PlainTextResponse) +def stress_template(): + """Download a CSV template for stress batch upload.""" + return PlainTextResponse( + content=stress_csv_template(), + media_type="text/csv", + headers={"Content-Disposition": "attachment; filename=stress_template.csv"}, + ) + + +@app.get("/api/batch/template/earnings", response_class=PlainTextResponse) +def earnings_template(): + """Download a CSV template for earnings batch upload.""" + return PlainTextResponse( + content=earnings_csv_template(), + media_type="text/csv", + headers={"Content-Disposition": "attachment; filename=earnings_template.csv"}, + ) + + +# ── Single-row manual prediction ────────────────────────────────────────── + +@app.post("/api/predict/stress") +def predict_stress(payload: dict): + """Submit a single row of sensor features → get stress prediction.""" + try: + result = predict_stress_row(payload) + return result + except Exception as e: + raise HTTPException(400, str(e)) + + +@app.post("/api/predict/earnings") +def predict_earnings(payload: dict): + """Submit a single row of earnings features → get velocity prediction.""" + try: + result = predict_earnings_row(payload) + return result + except Exception as e: + raise HTTPException(400, str(e)) + + +@app.get("/api/features/stress") +def stress_features(): + """Return the list of stress model input features with defaults.""" + features = [ + {"name": "motion_max", "label": "Motion Max (g)", "default": 1.5, "group": "Motion"}, + {"name": "motion_mean", "label": "Motion Mean (g)", "default": 0.8, "group": "Motion"}, + {"name": "motion_p95", "label": "Motion P95 (g)", "default": 1.2, "group": "Motion"}, + {"name": "motion_std", "label": "Motion Std (g)", "default": 0.3, "group": "Motion"}, + {"name": "brake_intensity", "label": "Brake Intensity", "default": 0.5, "group": "Motion"}, + {"name": "lateral_max", "label": "Lateral Max (g)", "default": 0.8, "group": "Motion"}, + {"name": "z_dev_max", "label": "Z Deviation Max", "default": 0.5, "group": "Motion"}, + {"name": "speed_mean", "label": "Speed Mean (km/h)", "default": 30.0, "group": "Speed"}, + {"name": "speed_at_brake", "label": "Speed at Brake (km/h)", "default": 25.0, "group": "Speed"}, + {"name": "speed_drop", "label": "Speed Drop (km/h)", "default": 5.0, "group": "Speed"}, + {"name": "spikes_above3", "label": "Spikes > 3g", "default": 0, "group": "Motion"}, + {"name": "spikes_above5", "label": "Spikes > 5g", "default": 0, "group": "Motion"}, + {"name": "audio_db_max", "label": "Audio dB Max", "default": 65.0, "group": "Audio"}, + {"name": "audio_db_mean", "label": "Audio dB Mean", "default": 55.0, "group": "Audio"}, + {"name": "audio_db_p90", "label": "Audio dB P90", "default": 62.0, "group": "Audio"}, + {"name": "audio_db_std", "label": "Audio dB Std", "default": 5.0, "group": "Audio"}, + {"name": "audio_class_max", "label": "Audio Class Max", "default": 2.0, "group": "Audio"}, + {"name": "audio_class_mean", "label": "Audio Class Mean", "default": 1.0, "group": "Audio"}, + {"name": "sustained_max", "label": "Sustained Max", "default": 10.0, "group": "Audio"}, + {"name": "sustained_sum", "label": "Sustained Sum", "default": 50.0, "group": "Audio"}, + {"name": "cadence_var_mean", "label": "Cadence Var Mean", "default": 0.3, "group": "Voice"}, + {"name": "cadence_var_max", "label": "Cadence Var Max", "default": 0.6, "group": "Voice"}, + {"name": "argument_frac", "label": "Argument Fraction", "default": 0.0, "group": "Voice"}, + {"name": "loud_frac", "label": "Loud Fraction", "default": 0.1, "group": "Voice"}, + {"name": "audio_leads_motion", "label": "Audio Leads Motion (s)", "default": 0.0, "group": "Timing"}, + {"name": "audio_onset_sec", "label": "Audio Onset (s)", "default": 15.0, "group": "Timing"}, + {"name": "brake_t_sec", "label": "Brake Time (s)", "default": 15.0, "group": "Timing"}, + {"name": "is_low_speed", "label": "Is Low Speed (0/1)", "default": 0, "group": "Flags"}, + {"name": "both_elevated", "label": "Both Elevated (0/1)", "default": 0, "group": "Flags"}, + {"name": "audio_only", "label": "Audio Only (0/1)", "default": 0, "group": "Flags"}, + ] + return {"features": features, "total": len(features)} + + +@app.get("/api/features/earnings") +def earnings_features(): + """Return the list of earnings model input features with defaults.""" + features = [ + {"name": "avg_earnings_per_hour", "label": "Avg Earnings/hr (₹)", "default": 250.0, "group": "Earnings"}, + {"name": "experience_months", "label": "Experience (months)", "default": 12, "group": "Driver"}, + {"name": "rating", "label": "Rating (1-5)", "default": 4.5, "group": "Driver"}, + {"name": "target_earnings", "label": "Target Earnings (₹)", "default": 2000.0, "group": "Earnings"}, + {"name": "remaining_earnings", "label": "Remaining Earnings (₹)", "default": 1200.0, "group": "Earnings"}, + {"name": "remaining_hours", "label": "Remaining Hours", "default": 5.0, "group": "Time"}, + {"name": "required_velocity", "label": "Required Velocity (₹/hr)", "default": 240.0, "group": "Earnings"}, + {"name": "trips_completed", "label": "Trips Completed", "default": 5, "group": "Trip"}, + {"name": "trip_rate", "label": "Trip Rate (trips/hr)", "default": 2.0, "group": "Trip"}, + {"name": "hour_of_day", "label": "Hour of Day (0-23)", "default": 14, "group": "Time"}, + {"name": "is_morning_rush", "label": "Morning Rush (0/1)", "default": 0, "group": "Time"}, + {"name": "is_lunch_rush", "label": "Lunch Rush (0/1)", "default": 0, "group": "Time"}, + {"name": "is_evening_rush", "label": "Evening Rush (0/1)", "default": 0, "group": "Time"}, + {"name": "current_velocity", "label": "Current Velocity (₹/hr)", "default": 200.0, "group": "Earnings"}, + ] + return {"features": features, "total": len(features)} + + +if __name__ == "__main__": + import uvicorn + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..b43a745 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,4 @@ +fastapi>=0.104 +uvicorn>=0.24 +pydantic>=2.0 +python-dateutil>=2.8 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..011ae1e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,18 @@ + + + + + + DrivePulse — Driver Wellness & Earnings + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..302a2bd --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3096 @@ +{ + "name": "drivepulse-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "drivepulse-frontend", + "version": "1.0.0", + "dependencies": { + "date-fns": "^3.2.0", + "leaflet": "^1.9.4", + "lucide-react": "^0.302.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-leaflet": "^4.2.1", + "react-router-dom": "^6.21.0", + "recharts": "^2.10.0" + }, + "devDependencies": { + "@types/leaflet": "^1.9.8", + "@vitejs/plugin-react": "^4.2.0", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.4.0", + "vite": "^5.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.302.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.302.0.tgz", + "integrity": "sha512-JZX+1fjpqxvQmEgItvPOAwRlqf0Eg9dSZMxljA2/V2M6dluOhQCPBhewIlSJWgkNu0M36kViOgmTAMnDaAMOFw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..0bd8f65 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "drivepulse-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.0", + "recharts": "^2.10.0", + "react-leaflet": "^4.2.1", + "leaflet": "^1.9.4", + "lucide-react": "^0.302.0", + "date-fns": "^3.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.2.0", + "vite": "^5.0.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.32", + "autoprefixer": "^10.4.16", + "@types/leaflet": "^1.9.8" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..fd43534 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,25 @@ +import { Routes, Route } from 'react-router-dom' +import Layout from './components/Layout' +import Dashboard from './pages/Dashboard' +import Trips from './pages/Trips' +import TripDetail from './pages/TripDetail' +import Trends from './pages/Trends' +import Goals from './pages/Goals' +import BatchUpload from './pages/BatchUpload' +import Predict from './pages/Predict' + +export default function App() { + return ( + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ) +} diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js new file mode 100644 index 0000000..b650d5d --- /dev/null +++ b/frontend/src/api/client.js @@ -0,0 +1,47 @@ +const BASE = '/api'; + +async function request(path, options = {}) { + const res = await fetch(`${BASE}${path}`, { + headers: { 'Content-Type': 'application/json', ...options.headers }, + ...options, + }); + if (!res.ok) throw new Error(`API ${res.status}: ${res.statusText}`); + return res.json(); +} + +export const api = { + // Dashboard + getDashboard: () => request('/dashboard'), + getProfile: () => request('/profile'), + + // Trips + getTrips: (params = {}) => { + const qs = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => { + if (v !== undefined && v !== null && v !== '') qs.set(k, v); + }); + const q = qs.toString(); + return request(`/trips${q ? '?' + q : ''}`); + }, + getTrip: (id) => request(`/trips/${id}`), + getSampleTrip: () => request('/sample-trip'), + + // Events + getTripEvents: (tripId) => request(`/trips/${tripId}/events`), + postFeedback: (eventId, label, comment = null) => + request(`/events/${eventId}/feedback`, { + method: 'POST', + body: JSON.stringify({ label, comment }), + }), + + // Goals + getGoals: () => request('/goals'), + setGoal: (daily_target) => + request('/goals', { method: 'POST', body: JSON.stringify({ daily_target }) }), + + // Metrics + getMetrics: (range = '7d') => request(`/metrics?range=${range}`), + + // Tips + getTips: () => request('/tips'), +}; diff --git a/frontend/src/components/ConfidenceBadge.jsx b/frontend/src/components/ConfidenceBadge.jsx new file mode 100644 index 0000000..9fe323c --- /dev/null +++ b/frontend/src/components/ConfidenceBadge.jsx @@ -0,0 +1,15 @@ +const levelConfig = { + low: { bg: 'bg-green-100', text: 'text-green-800', label: 'Low' }, + medium: { bg: 'bg-yellow-100', text: 'text-yellow-800', label: 'Med' }, + high: { bg: 'bg-red-100', text: 'text-red-800', label: 'High' }, +} + +export default function ConfidenceBadge({ level, score }) { + const cfg = levelConfig[level] || levelConfig.low + return ( + + {cfg.label} + {score !== undefined && {Math.round(score * 100)}%} + + ) +} diff --git a/frontend/src/components/EarningsProgress.jsx b/frontend/src/components/EarningsProgress.jsx new file mode 100644 index 0000000..d8ed3c0 --- /dev/null +++ b/frontend/src/components/EarningsProgress.jsx @@ -0,0 +1,54 @@ +export default function EarningsProgress({ goals }) { + if (!goals) return null + const pct = Math.min(100, Math.round((goals.current_earnings / goals.daily_target) * 100)) + + const statusColor = { + ahead: 'text-uber-green', + on_track: 'text-uber-blue', + at_risk: 'text-uber-red', + }[goals.forecast_status] || 'text-uber-gray-500' + + return ( +
+
+

Daily Earnings Target

+ + {goals.forecast_status?.replace('_', ' ')} + +
+ +
+ ₹{goals.current_earnings.toLocaleString()} + / ₹{goals.daily_target.toLocaleString()} +
+ + {/* Progress bar */} +
+
+
+
+ {pct}% achieved + ₹{(goals.daily_target - goals.current_earnings).toLocaleString()} remaining +
+ + {/* Extra stats */} +
+
+

₹{goals.current_velocity}

+

Current ₹/hr

+
+
+

₹{goals.required_velocity}

+

Required ₹/hr

+
+
+

{Math.round(goals.goal_probability * 100)}%

+

Probability

+
+
+
+ ) +} diff --git a/frontend/src/components/EventCard.jsx b/frontend/src/components/EventCard.jsx new file mode 100644 index 0000000..be8cab2 --- /dev/null +++ b/frontend/src/components/EventCard.jsx @@ -0,0 +1,70 @@ +import { useState } from 'react' +import ConfidenceBadge from './ConfidenceBadge' +import FeedbackButtons from './FeedbackButtons' +import ExplainModal from './ExplainModal' +import { Info, MapPin } from 'lucide-react' + +export default function EventCard({ event, onJumpTo, onFeedback }) { + const [showExplain, setShowExplain] = useState(false) + + const severityColor = { + low: 'border-l-uber-green', + medium: 'border-l-uber-yellow', + high: 'border-l-uber-red', + }[event.severity] || 'border-l-uber-gray-300' + + const time = event.timestamp + ? new Date(event.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) + : `+${Math.round(event.offset_sec / 60)}m` + + return ( + <> +
+
+
+
+ {event.emoji} + {event.label.replace(/_/g, ' ')} + +
+

{time}

+ {event.explain?.summary && ( +

{event.explain.summary}

+ )} +
+ +
+ + {onJumpTo && ( + + )} +
+
+ +
+ +
+
+ + {showExplain && ( + setShowExplain(false)} /> + )} + + ) +} diff --git a/frontend/src/components/ExplainModal.jsx b/frontend/src/components/ExplainModal.jsx new file mode 100644 index 0000000..2670510 --- /dev/null +++ b/frontend/src/components/ExplainModal.jsx @@ -0,0 +1,73 @@ +import { X } from 'lucide-react' + +export default function ExplainModal({ event, onClose }) { + const { explain } = event + if (!explain) return null + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+
+

Why This Happened

+

+ {event.emoji} {event.label.replace(/_/g, ' ')} +

+
+ +
+ + {/* Top 3 feature contributions */} +
+

Top Feature Contributions

+
+ {explain.top_features?.map((f, i) => { + const pct = Math.round(f.contribution * 100) + const barColor = i === 0 ? 'bg-uber-red' : i === 1 ? 'bg-uber-orange' : 'bg-uber-yellow' + return ( +
+
+ + {f.feature.replace(/_/g, ' ')} {f.direction} + + {pct}% +
+
+
+
+
+ ) + })} +
+
+ + {/* Model inputs */} +
+

Model Inputs (snapshot)

+
+ {explain.model_inputs && + Object.entries(explain.model_inputs).map(([k, v]) => ( +
+ {k.replace(/_/g, ' ')} + {v} +
+ ))} +
+
+ + {/* Summary */} + {explain.summary && ( +
+

{explain.summary}

+
+ )} +
+
+ ) +} diff --git a/frontend/src/components/FeedbackButtons.jsx b/frontend/src/components/FeedbackButtons.jsx new file mode 100644 index 0000000..d705f50 --- /dev/null +++ b/frontend/src/components/FeedbackButtons.jsx @@ -0,0 +1,47 @@ +import { useState } from 'react' +import { ThumbsUp, ThumbsDown, MinusCircle } from 'lucide-react' +import { api } from '../api/client' + +const options = [ + { key: 'correct', label: 'Correct', icon: ThumbsUp, color: 'text-uber-green hover:bg-green-50' }, + { key: 'incorrect', label: 'Incorrect', icon: ThumbsDown, color: 'text-uber-red hover:bg-red-50' }, + { key: 'not_relevant', label: 'Not Relevant', icon: MinusCircle, color: 'text-uber-gray-500 hover:bg-uber-gray-100' }, +] + +export default function FeedbackButtons({ eventId, current, onFeedback }) { + const [selected, setSelected] = useState(current || null) + const [sending, setSending] = useState(false) + + const handleClick = async (label) => { + if (sending) return + setSending(true) + try { + await api.postFeedback(eventId, label) + setSelected(label) + if (onFeedback) onFeedback(eventId, label) + } catch { + // silent + } finally { + setSending(false) + } + } + + return ( +
+ Feedback: + {options.map(({ key, label, icon: Icon, color }) => ( + + ))} +
+ ) +} diff --git a/frontend/src/components/FilterChips.jsx b/frontend/src/components/FilterChips.jsx new file mode 100644 index 0000000..b079c0d --- /dev/null +++ b/frontend/src/components/FilterChips.jsx @@ -0,0 +1,27 @@ +const presets = [ + { key: 'high_stress', label: 'High Stress', emoji: '🔴' }, + { key: 'high_earnings', label: 'High Earnings', emoji: '💰' }, + { key: 'night', label: 'Night Trips', emoji: '🌙' }, + { key: 'short', label: 'Short Trips', emoji: '⚡' }, +] + +export default function FilterChips({ active, onSelect }) { + return ( +
+ {presets.map(p => ( + + ))} +
+ ) +} diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx new file mode 100644 index 0000000..13f7771 --- /dev/null +++ b/frontend/src/components/Layout.jsx @@ -0,0 +1,13 @@ +import { Outlet } from 'react-router-dom' +import Sidebar from './Sidebar' + +export default function Layout() { + return ( +
+ +
+ +
+
+ ) +} diff --git a/frontend/src/components/SampleTripCard.jsx b/frontend/src/components/SampleTripCard.jsx new file mode 100644 index 0000000..ea9fd45 --- /dev/null +++ b/frontend/src/components/SampleTripCard.jsx @@ -0,0 +1,40 @@ +import { useNavigate } from 'react-router-dom' +import { Play } from 'lucide-react' +import { api } from '../api/client' +import { useState } from 'react' + +export default function SampleTripCard() { + const navigate = useNavigate() + const [loading, setLoading] = useState(false) + + const handlePlay = async () => { + setLoading(true) + try { + const trip = await api.getSampleTrip() + navigate(`/trips/${trip.id}`) + } catch { + alert('Failed to load sample trip') + } finally { + setLoading(false) + } + } + + return ( +
+

Quick Start

+

Explore a Sample Trip

+

+ See stress detection, earnings tracking, and event explainability in action. +

+ +
+ ) +} diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx new file mode 100644 index 0000000..0cbc193 --- /dev/null +++ b/frontend/src/components/Sidebar.jsx @@ -0,0 +1,49 @@ +import { NavLink } from 'react-router-dom' +import { LayoutDashboard, MapPin, TrendingUp, Target, Activity, Upload, PenLine } from 'lucide-react' + +const links = [ + { to: '/', label: 'Dashboard', icon: LayoutDashboard }, + { to: '/trips', label: 'Trips', icon: MapPin }, + { to: '/trends', label: 'Trends', icon: TrendingUp }, + { to: '/goals', label: 'Goals', icon: Target }, + { to: '/predict', label: 'Predict', icon: PenLine }, + { to: '/batch', label: 'Batch Upload', icon: Upload }, +] + +export default function Sidebar() { + return ( + + ) +} diff --git a/frontend/src/components/SignalCharts.jsx b/frontend/src/components/SignalCharts.jsx new file mode 100644 index 0000000..f4ada29 --- /dev/null +++ b/frontend/src/components/SignalCharts.jsx @@ -0,0 +1,61 @@ +import { + LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, +} from 'recharts' + +export default function SignalCharts({ signals, cursorTime }) { + if (!signals || !signals.timestamps) return null + + const data = signals.timestamps.map((t, i) => ({ + time: t, + timeLabel: `${Math.floor(t / 60)}:${String(t % 60).padStart(2, '0')}`, + speed: signals.speed[i], + accel: signals.accel_magnitude[i], + audio: signals.audio_db[i], + })) + + const charts = [ + { key: 'speed', label: 'Speed (km/h)', color: '#276EF1', domain: [0, 80] }, + { key: 'accel', label: 'Accel Magnitude (g)', color: '#E11900', domain: [0, 8] }, + { key: 'audio', label: 'Audio (dB)', color: '#FF6937', domain: [30, 100] }, + ] + + return ( +
+ {charts.map(({ key, label, color, domain }) => ( +
+

{label}

+ + + + + + `Time: ${v}`} + /> + + {cursorTime !== undefined && ( + null} + stroke="transparent" + /> + )} + + +
+ ))} +
+ ) +} diff --git a/frontend/src/components/StressTips.jsx b/frontend/src/components/StressTips.jsx new file mode 100644 index 0000000..1d6c573 --- /dev/null +++ b/frontend/src/components/StressTips.jsx @@ -0,0 +1,39 @@ +import { useState, useEffect } from 'react' +import { api } from '../api/client' +import { Lightbulb, X } from 'lucide-react' + +export default function StressTips() { + const [tips, setTips] = useState([]) + const [dismissed, setDismissed] = useState(new Set()) + + useEffect(() => { + api.getTips().then(res => setTips(res.tips)).catch(() => {}) + }, []) + + const visible = tips.filter(t => !dismissed.has(t.id)) + if (visible.length === 0) return null + + return ( +
+

+ + Stress-Reduction Tips +

+ {visible.map(tip => ( +
+ +

{tip.title}

+

{tip.text}

+ +
+ ))} +
+ ) +} diff --git a/frontend/src/components/SummaryCard.jsx b/frontend/src/components/SummaryCard.jsx new file mode 100644 index 0000000..a06fae3 --- /dev/null +++ b/frontend/src/components/SummaryCard.jsx @@ -0,0 +1,14 @@ +export default function SummaryCard({ icon: Icon, label, value, sub, color = 'text-uber-black' }) { + return ( +
+
+ +
+
+

{label}

+

{value}

+ {sub &&

{sub}

} +
+
+ ) +} diff --git a/frontend/src/components/TimelineSlider.jsx b/frontend/src/components/TimelineSlider.jsx new file mode 100644 index 0000000..4e0c26c --- /dev/null +++ b/frontend/src/components/TimelineSlider.jsx @@ -0,0 +1,65 @@ +import { Play, Pause, SkipBack, SkipForward } from 'lucide-react' +import { useState, useEffect, useRef } from 'react' + +export default function TimelineSlider({ maxSec, currentSec, onChange, isPlaying, onPlayPause }) { + const pct = maxSec > 0 ? (currentSec / maxSec) * 100 : 0 + + const formatTime = (s) => { + const m = Math.floor(s / 60) + const sec = Math.floor(s % 60) + return `${m}:${String(sec).padStart(2, '0')}` + } + + return ( +
+
+ + + + + + + {formatTime(currentSec)} + + {/* Slider */} +
+
+
+
+ onChange(Number(e.target.value))} + className="absolute inset-x-0 w-full h-6 opacity-0 cursor-pointer" + /> +
+
+ + {formatTime(maxSec)} +
+
+ ) +} diff --git a/frontend/src/components/TodayTimeline.jsx b/frontend/src/components/TodayTimeline.jsx new file mode 100644 index 0000000..9e6d486 --- /dev/null +++ b/frontend/src/components/TodayTimeline.jsx @@ -0,0 +1,60 @@ +import { useNavigate } from 'react-router-dom' + +const stressColors = { + low: 'bg-uber-green/20 border-uber-green', + medium: 'bg-uber-yellow/20 border-uber-yellow', + high: 'bg-uber-red/20 border-uber-red', +} + +export default function TodayTimeline({ trips }) { + const navigate = useNavigate() + if (!trips || trips.length === 0) return null + + const minH = Math.min(...trips.map(t => new Date(t.start_time).getHours())) + const maxH = Math.max(...trips.map(t => new Date(t.end_time).getHours())) + 1 + const span = Math.max(maxH - minH, 1) + + return ( +
+

Today Timeline

+ + {/* Hour labels */} +
+
+ {Array.from({ length: span + 1 }, (_, i) => { + const h = minH + i + return {h % 12 || 12}{h < 12 ? 'a' : 'p'} + })} +
+ + {/* Track */} +
+ {trips.map((t) => { + const startH = new Date(t.start_time).getHours() + new Date(t.start_time).getMinutes() / 60 + const endH = new Date(t.end_time).getHours() + new Date(t.end_time).getMinutes() / 60 + const left = ((startH - minH) / span) * 100 + const width = Math.max(((endH - startH) / span) * 100, 2) + + return ( +
+
+ +
+ Low + Medium + High +
+
+ ) +} diff --git a/frontend/src/components/TripListItem.jsx b/frontend/src/components/TripListItem.jsx new file mode 100644 index 0000000..ddc956e --- /dev/null +++ b/frontend/src/components/TripListItem.jsx @@ -0,0 +1,77 @@ +import { useNavigate } from 'react-router-dom' +import { Clock, DollarSign, AlertTriangle, ChevronRight } from 'lucide-react' +import ConfidenceBadge from './ConfidenceBadge' + +const stressDot = { + low: 'bg-uber-green', + medium: 'bg-uber-yellow', + high: 'bg-uber-red', +} + +export default function TripListItem({ trip }) { + const navigate = useNavigate() + const start = new Date(trip.start_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + const end = new Date(trip.end_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + + return ( + + ) +} diff --git a/frontend/src/components/TripMap.jsx b/frontend/src/components/TripMap.jsx new file mode 100644 index 0000000..ce34f1a --- /dev/null +++ b/frontend/src/components/TripMap.jsx @@ -0,0 +1,106 @@ +import { useEffect, useRef } from 'react' +import L from 'leaflet' +import 'leaflet/dist/leaflet.css' + +// Fix Leaflet default icons +delete L.Icon.Default.prototype._getIconUrl +L.Icon.Default.mergeOptions({ + iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png', + iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png', + shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png', +}) + +const severityColor = { low: '#06C167', medium: '#FFC043', high: '#E11900' } + +const eventIcon = (severity) => + L.divIcon({ + html: `
`, + className: '', + iconSize: [14, 14], + iconAnchor: [7, 7], + }) + +const cursorIcon = L.divIcon({ + html: `
`, + className: '', + iconSize: [18, 18], + iconAnchor: [9, 9], +}) + +export default function TripMap({ route, events, cursorIndex }) { + const mapRef = useRef(null) + const mapInstance = useRef(null) + const cursorMarker = useRef(null) + + useEffect(() => { + if (!mapRef.current || !route || route.length === 0) return + + // Init map + if (!mapInstance.current) { + mapInstance.current = L.map(mapRef.current, { + scrollWheelZoom: true, + zoomControl: true, + }) + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OSM', + }).addTo(mapInstance.current) + } + + const map = mapInstance.current + + // Clear layers except tiles + map.eachLayer(layer => { + if (!(layer instanceof L.TileLayer)) map.removeLayer(layer) + }) + + // Polyline + const polyline = L.polyline(route, { + color: '#276EF1', + weight: 4, + opacity: 0.8, + }).addTo(map) + + // Start / End + L.marker(route[0], { + icon: L.divIcon({ + html: `
`, + className: '', iconSize: [12, 12], iconAnchor: [6, 6], + }), + }).addTo(map).bindPopup('Start') + + L.marker(route[route.length - 1], { + icon: L.divIcon({ + html: `
`, + className: '', iconSize: [12, 12], iconAnchor: [6, 6], + }), + }).addTo(map).bindPopup('End') + + // Event markers + events?.forEach(ev => { + if (ev.location) { + L.marker(ev.location, { icon: eventIcon(ev.severity) }) + .addTo(map) + .bindPopup(`${ev.emoji} ${ev.label.replace(/_/g, ' ')}
Confidence: ${Math.round(ev.confidence * 100)}%`) + } + }) + + map.fitBounds(polyline.getBounds(), { padding: [30, 30] }) + + // Cursor marker + cursorMarker.current = L.marker(route[0], { icon: cursorIcon }).addTo(map) + + return () => { + // don't destroy map, just clean up + } + }, [route, events]) + + // Update cursor position + useEffect(() => { + if (cursorMarker.current && route && cursorIndex !== undefined) { + const idx = Math.min(Math.max(0, cursorIndex), route.length - 1) + cursorMarker.current.setLatLng(route[idx]) + } + }, [cursorIndex, route]) + + return
+} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..9884599 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,22 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +/* Custom scrollbar */ +::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: #CBCBCB; border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: #AFAFAF; } + +/* Leaflet overrides */ +.leaflet-container { border-radius: 12px; } diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..b15cdd6 --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,13 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + + + , +) diff --git a/frontend/src/pages/BatchUpload.jsx b/frontend/src/pages/BatchUpload.jsx new file mode 100644 index 0000000..88a3e5a --- /dev/null +++ b/frontend/src/pages/BatchUpload.jsx @@ -0,0 +1,577 @@ +import { useState, useRef } from 'react' +import { Upload, Download, FileSpreadsheet, Activity, DollarSign, AlertTriangle, ChevronDown, ChevronUp, Info, BarChart3, CheckCircle, XCircle, Bell } from 'lucide-react' +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, + PieChart, Pie, Cell, Legend, +} from 'recharts' + +const SEVERITY_COLORS = { low: '#06C167', medium: '#FFC043', high: '#E11900' } +const FORECAST_COLORS = { ahead: '#06C167', on_track: '#276EF1', at_risk: '#E11900' } +const SITUATION_COLORS = { + NORMAL: '#06C167', TRAFFIC_STOP: '#276EF1', SPEED_BREAKER: '#FFC043', + CONFLICT: '#E11900', ESCALATING: '#A3000B', ARGUMENT_ONLY: '#FF6937', MUSIC_OR_CALL: '#7356BF', +} + +export default function BatchUpload() { + const [mode, setMode] = useState('stress') // 'stress' | 'earnings' + const [file, setFile] = useState(null) + const [loading, setLoading] = useState(false) + const [result, setResult] = useState(null) + const [error, setError] = useState(null) + const [expandedRow, setExpandedRow] = useState(null) + const inputRef = useRef(null) + + const handleUpload = async () => { + if (!file) return + setLoading(true) + setError(null) + setResult(null) + + const formData = new FormData() + formData.append('file', file) + + try { + const endpoint = mode === 'stress' ? '/api/batch/stress' : '/api/batch/earnings' + const res = await fetch(endpoint, { method: 'POST', body: formData }) + if (!res.ok) { + const text = await res.text() + throw new Error(text || `Upload failed (${res.status})`) + } + const data = await res.json() + setResult(data) + } catch (err) { + setError(err.message || 'Upload failed') + } finally { + setLoading(false) + } + } + + const downloadTemplate = (type) => { + const a = document.createElement('a') + a.href = `/api/batch/template/${type}` + a.download = `${type}_template.csv` + a.click() + } + + const downloadResults = () => { + if (!result) return + const json = JSON.stringify(result, null, 2) + const blob = new Blob([json], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `${mode}_batch_results.json` + a.click() + URL.revokeObjectURL(url) + } + + const downloadResultsCSV = () => { + if (!result?.results?.length) return + const rows = result.results + const keys = Object.keys(rows[0]).filter(k => typeof rows[0][k] !== 'object') + const header = keys.join(',') + const lines = rows.map(r => keys.map(k => { + const v = r[k] + return v === null || v === undefined ? '' : v + }).join(',')) + const csv = header + '\n' + lines.join('\n') + const blob = new Blob([csv], { type: 'text/csv' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `${mode}_batch_results.csv` + a.click() + URL.revokeObjectURL(url) + } + + return ( +
+ {/* Header */} +
+

Batch CSV Upload

+

+ Upload a CSV file with multiple trip windows to get stress & earnings predictions in bulk — no manual entry needed. +

+
+ + {/* Mode toggle */} +
+ {[ + { key: 'stress', label: 'Stress Detection', icon: Activity }, + { key: 'earnings', label: 'Earnings Forecast', icon: DollarSign }, + ].map(({ key, label, icon: Icon }) => ( + + ))} +
+ + {/* Template + Upload card */} +
+ {/* Template download */} +
+
+ +

CSV Format

+
+

+ {mode === 'stress' + ? 'Each row = one 30-second sensor window. Requires 30 feature columns (motion, audio, speed aggregates). Add optional trip_id and timestamp columns for identification.' + : 'Each row = one earnings velocity log entry. Requires driver_id, timestamp, cumulative_earnings, elapsed_hours, current_velocity, target_velocity, velocity_delta, trips_completed, target_earnings.' + } +

+
+

Required columns

+
+ {(mode === 'stress' + ? ['motion_max', 'motion_mean', 'motion_p95', 'brake_intensity', 'audio_db_max', 'audio_db_p90', 'speed_mean', '...+23 more'] + : ['driver_id', 'timestamp', 'cumulative_earnings', 'elapsed_hours', 'current_velocity', 'target_velocity', 'trips_completed', 'target_earnings'] + ).map(col => ( + + {col} + + ))} +
+
+ +
+ + {/* File upload */} +
+

Upload CSV

+ + {/* Drop zone */} +
inputRef.current?.click()} + onDragOver={(e) => e.preventDefault()} + onDrop={(e) => { e.preventDefault(); const f = e.dataTransfer.files[0]; if (f) setFile(f) }} + className="border-2 border-dashed border-uber-gray-200 rounded-xl p-8 text-center cursor-pointer + hover:border-uber-blue hover:bg-blue-50/30 transition-colors" + > + +

+ {file ? ( + {file.name} + ) : ( + <>Drop CSV here or click to browse + )} +

+

Supports .csv files

+ { if (e.target.files[0]) setFile(e.target.files[0]) }} + /> +
+ + {file && ( +
+
+ + {file.name} + ({(file.size / 1024).toFixed(1)} KB) +
+ +
+ )} + + +
+
+ + {/* Error */} + {error && ( +
+ Error: {error} +
+ )} + + {/* Results */} + {result && mode === 'stress' && } + {result && mode === 'earnings' && } + + {/* Export buttons */} + {result && ( +
+ + +
+ )} +
+ ) +} + + +/* ── Stress Results ─────────────────────────────────────── */ + +function StressResults({ result, expandedRow, setExpandedRow }) { + const { summary, results } = result + + const pieData = Object.entries(summary.situation_counts || {}).map(([name, count]) => ({ + name, value: count, fill: SITUATION_COLORS[name] || '#AFAFAF', + })) + const severityData = Object.entries(summary.severity_counts || {}).map(([name, count]) => ({ + name: name.charAt(0).toUpperCase() + name.slice(1), value: count, fill: SEVERITY_COLORS[name], + })) + + return ( + <> + {/* Summary cards */} +
+ + + + + 3 ? 'text-uber-red' : 'text-uber-green'} /> +
+ + {/* Charts */} +
+
+

Situation Distribution

+ + + `${name.replace(/_/g, ' ')} (${value})`}> + {pieData.map((d, i) => )} + + + + +
+
+

Severity Breakdown

+ + + + + + + + {severityData.map((d, i) => )} + + + +
+
+ + {/* Results table */} +
+
+

Per-Window Results ({results.length})

+
+
+ + + + + + + + + + + + + + {results.map((r, i) => ( + setExpandedRow(expandedRow === i ? null : i)} /> + ))} + +
#TripSituationSeverityConfidenceNotifyDetails
+
+
+ + ) +} + +function StressRow({ r, i, expanded, onToggle }) { + return ( + <> + + {r.row_index + 1} + {r.trip_id || r.timestamp || '-'} + + + {r.emoji} + {r.situation_name?.replace(/_/g, ' ')} + + + + + {r.severity} + + + +
+
+
= 0.75 ? 'bg-uber-green' : r.confidence >= 0.5 ? 'bg-uber-yellow' : 'bg-uber-red'}`} + style={{ width: `${Math.round(r.confidence * 100)}%` }} + /> +
+ {Math.round(r.confidence * 100)}% +
+ + + {r.should_notify ? : } + + + + + + {expanded && ( + + +
+ {/* Top features */} + {r.top_features?.length > 0 && ( +
+

Top Feature Deviations

+
+ {r.top_features.map((f, j) => ( +
+ {f.feature} +
+
+
+ z={f.z_score} + val={f.value} +
+ ))} +
+
+ )} + {/* All probabilities */} + {r.all_probabilities && Object.keys(r.all_probabilities).length > 0 && ( +
+

Class Probabilities

+
+ {Object.entries(r.all_probabilities).sort(([,a], [,b]) => b - a).map(([cls, prob]) => ( +
+ {cls.replace(/_/g, ' ')} +
+
+
+ {(prob * 100).toFixed(1)}% +
+ ))} +
+
+ )} +
+ + + )} + + ) +} + + +/* ── Earnings Results ───────────────────────────────────── */ + +function EarningsResults({ result, expandedRow, setExpandedRow }) { + const { summary, results } = result + + const forecastData = Object.entries(summary.forecast_counts || {}).map(([name, count]) => ({ + name: name.replace(/_/g, ' ').replace(/^\w/, c => c.toUpperCase()), + value: count, + fill: FORECAST_COLORS[name] || '#AFAFAF', + })) + + const velocityTrend = results.map((r, i) => ({ + idx: i + 1, + predicted: r.predicted_velocity, + target: r.target_velocity, + current: r.current_velocity, + hour: r.hour_of_day ?? i, + })) + + return ( + <> + {/* Summary cards */} +
+ + + + +
+ + {/* Charts */} +
+
+

Predicted vs Target Velocity

+ + + + + `₹${v}`} /> + [`₹${v}`, '']} /> + + + + + +
+
+

Forecast Status

+ + + `${name} (${value})`} + > + {forecastData.map((d, i) => )} + + + + +
+
+ + {/* Results table */} +
+
+

Per-Entry Results ({results.length})

+
+
+ + + + + + + + + + + + + + + {results.map((r, i) => ( + setExpandedRow(expandedRow === i ? null : i)} /> + ))} + +
#DriverHourPredicted ₹/hrTarget ₹/hrStatusProgressDetails
+
+
+ + ) +} + +function EarningsRow({ r, i, expanded, onToggle }) { + const statusColor = { + ahead: 'bg-green-100 text-green-700', + on_track: 'bg-blue-100 text-blue-700', + at_risk: 'bg-red-100 text-red-700', + } + return ( + <> + + {r.row_index + 1} + {r.driver_id || '-'} + {r.hour_of_day ?? '-'} + ₹{r.predicted_velocity} + ₹{r.target_velocity} + + + {r.forecast_status?.replace(/_/g, ' ')} + + + +
+
+
+
+ {r.pct_target}% +
+ + + + + + {expanded && ( + + +
+
Cumulative Earnings

₹{r.cumulative_earnings}

+
Elapsed Hours

{r.elapsed_hours}h

+
Remaining

₹{r.remaining_earnings}

+
Hours to Target

{r.hours_to_target ?? '—'}h

+
+ + + )} + + ) +} + + +/* ── Shared summary card ────────────────────────────────── */ + +function SumCard({ label, value, icon: Icon, color = 'text-uber-black' }) { + return ( +
+ +

{value}

+

{label}

+
+ ) +} diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx new file mode 100644 index 0000000..eb72a53 --- /dev/null +++ b/frontend/src/pages/Dashboard.jsx @@ -0,0 +1,103 @@ +import { useState, useEffect } from 'react' +import { api } from '../api/client' +import SummaryCard from '../components/SummaryCard' +import TodayTimeline from '../components/TodayTimeline' +import SampleTripCard from '../components/SampleTripCard' +import EarningsProgress from '../components/EarningsProgress' +import StressTips from '../components/StressTips' +import { Car, Clock, DollarSign, AlertTriangle, Target, Activity } from 'lucide-react' + +export default function Dashboard() { + const [dashboard, setDashboard] = useState(null) + const [trips, setTrips] = useState([]) + const [goals, setGoals] = useState(null) + const [selectedDay, setSelectedDay] = useState('today') + const [loading, setLoading] = useState(true) + + const today = '2026-03-08' + const yesterday = '2026-03-07' + + useEffect(() => { + loadData() + }, [selectedDay]) + + const loadData = async () => { + setLoading(true) + try { + const [dash, tripRes, goalsRes] = await Promise.all([ + api.getDashboard(), + api.getTrips({ date: selectedDay === 'today' ? today : yesterday }), + api.getGoals(), + ]) + setDashboard(dash) + setTrips(tripRes.trips) + setGoals(goalsRes) + } catch (err) { + console.error('Failed to load dashboard:', err) + } finally { + setLoading(false) + } + } + + if (loading && !dashboard) { + return ( +
+
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+

Dashboard

+

+ {selectedDay === 'today' ? 'Today' : 'Yesterday'}, {selectedDay === 'today' ? 'March 8, 2026' : 'March 7, 2026'} +

+
+ + {/* Today/Yesterday toggle */} +
+ {['today', 'yesterday'].map(day => ( + + ))} +
+
+ + {/* Quick Start */} + + + {/* Summary Cards */} + {dashboard && ( +
+ + + + + +
+ )} + + {/* Timeline + Earnings */} +
+ + +
+ + {/* Stress Tips */} + +
+ ) +} diff --git a/frontend/src/pages/Goals.jsx b/frontend/src/pages/Goals.jsx new file mode 100644 index 0000000..8e3899f --- /dev/null +++ b/frontend/src/pages/Goals.jsx @@ -0,0 +1,127 @@ +import { useState, useEffect } from 'react' +import { api } from '../api/client' +import EarningsProgress from '../components/EarningsProgress' +import StressTips from '../components/StressTips' +import { Target, Save, CheckCircle } from 'lucide-react' + +export default function Goals() { + const [goals, setGoals] = useState(null) + const [loading, setLoading] = useState(true) + const [targetInput, setTargetInput] = useState('') + const [saving, setSaving] = useState(false) + const [saved, setSaved] = useState(false) + + useEffect(() => { + api.getGoals() + .then(g => { + setGoals(g) + setTargetInput(g.daily_target) + }) + .catch(() => {}) + .finally(() => setLoading(false)) + }, []) + + const handleSave = async () => { + setSaving(true) + try { + const updated = await api.setGoal(Number(targetInput)) + setGoals(updated) + setSaved(true) + setTimeout(() => setSaved(false), 2000) + } catch { + alert('Failed to save goal') + } finally { + setSaving(false) + } + } + + if (loading) { + return ( +
+
+
+ ) + } + + return ( +
+

Personalization & Goals

+ + {/* Set target */} +
+
+ +

Daily Earnings Target

+
+ +
+
+ + setTargetInput(e.target.value)} + className="w-full pl-8 pr-4 py-3 border border-uber-gray-200 rounded-lg text-lg font-semibold outline-none focus:border-uber-blue transition" + placeholder="1800" + /> +
+ +
+ +

+ Set your daily earnings target. Your progress will be tracked in real-time on the dashboard. +

+
+ + {/* Current progress */} + + + {/* Goal milestones */} + {goals && ( +
+

Today's Progress

+
+ {[25, 50, 75, 100].map(pct => { + const amount = Math.round(goals.daily_target * pct / 100) + const reached = goals.current_earnings >= amount + return ( +
+
+ {reached ? '✓' : `${pct}%`} +
+
+

+ ₹{amount.toLocaleString()} milestone +

+
+ + {reached ? 'Reached' : 'Pending'} + +
+ ) + })} +
+
+ )} + + {/* Stress tips */} + +
+ ) +} diff --git a/frontend/src/pages/Predict.jsx b/frontend/src/pages/Predict.jsx new file mode 100644 index 0000000..d26924c --- /dev/null +++ b/frontend/src/pages/Predict.jsx @@ -0,0 +1,449 @@ +import { useState, useEffect } from 'react' +import { + Activity, DollarSign, Send, RotateCcw, AlertTriangle, CheckCircle, + ChevronDown, ChevronUp, Zap, Info, Loader2, +} from 'lucide-react' +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell, +} from 'recharts' + +const SEVERITY_BG = { low: 'bg-green-50 border-green-200', medium: 'bg-yellow-50 border-yellow-200', high: 'bg-red-50 border-red-200' } +const SEVERITY_TEXT = { low: 'text-green-700', medium: 'text-yellow-700', high: 'text-red-700' } +const SITUATION_COLORS = { + NORMAL: '#06C167', TRAFFIC_STOP: '#276EF1', SPEED_BREAKER: '#FFC043', + CONFLICT: '#E11900', ESCALATING: '#A3000B', ARGUMENT_ONLY: '#FF6937', MUSIC_OR_CALL: '#7356BF', +} +const FORECAST_BG = { ahead: 'bg-green-50 border-green-200', on_track: 'bg-blue-50 border-blue-200', at_risk: 'bg-red-50 border-red-200' } +const FORECAST_TEXT = { ahead: 'text-green-700', on_track: 'text-blue-700', at_risk: 'text-red-700' } + +export default function Predict() { + const [mode, setMode] = useState('stress') + const [featureDefs, setFeatureDefs] = useState([]) + const [values, setValues] = useState({}) + const [loading, setLoading] = useState(false) + const [loadingFeatures, setLoadingFeatures] = useState(true) + const [result, setResult] = useState(null) + const [error, setError] = useState(null) + const [collapsedGroups, setCollapsedGroups] = useState({}) + + // Load feature definitions when mode changes + useEffect(() => { + setLoadingFeatures(true) + setResult(null) + setError(null) + fetch(`/api/features/${mode}`) + .then(r => r.json()) + .then(data => { + setFeatureDefs(data.features) + const defaults = {} + data.features.forEach(f => { defaults[f.name] = f.default }) + setValues(defaults) + setCollapsedGroups({}) + }) + .catch(e => setError('Failed to load feature definitions')) + .finally(() => setLoadingFeatures(false)) + }, [mode]) + + const handleChange = (name, rawVal) => { + setValues(prev => ({ ...prev, [name]: rawVal === '' ? '' : Number(rawVal) })) + } + + const handleReset = () => { + const defaults = {} + featureDefs.forEach(f => { defaults[f.name] = f.default }) + setValues(defaults) + setResult(null) + setError(null) + } + + const handlePredict = async () => { + setLoading(true) + setError(null) + setResult(null) + try { + const payload = {} + featureDefs.forEach(f => { + payload[f.name] = values[f.name] === '' ? 0 : Number(values[f.name]) + }) + const res = await fetch(`/api/predict/${mode}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }) + if (!res.ok) { + const txt = await res.text() + throw new Error(txt || `Error ${res.status}`) + } + setResult(await res.json()) + } catch (e) { + setError(e.message) + } finally { + setLoading(false) + } + } + + const toggleGroup = (group) => { + setCollapsedGroups(prev => ({ ...prev, [group]: !prev[group] })) + } + + // Group features + const groups = {} + featureDefs.forEach(f => { + if (!groups[f.group]) groups[f.group] = [] + groups[f.group].push(f) + }) + + return ( +
+ {/* Header */} +
+

Manual Prediction

+

+ Type in sensor or earnings values directly to get a single prediction +

+
+ + {/* Mode toggle */} +
+ + +
+ + {loadingFeatures ? ( +
+ + Loading features… +
+ ) : ( +
+ {/* Input form — left 3 cols */} +
+
+
+

+ Input Features ({featureDefs.length}) +

+ +
+ +
+ {Object.entries(groups).map(([groupName, fields]) => ( +
+ {/* Group header */} + + + {/* Fields */} + {!collapsedGroups[groupName] && ( +
+ {fields.map(f => ( +
+ + handleChange(f.name, e.target.value)} + className="w-full px-3 py-2 rounded-lg border border-uber-gray-200 text-sm text-uber-gray-800 focus:outline-none focus:ring-2 focus:ring-uber-blue focus:border-transparent transition-all" + placeholder={String(f.default)} + /> +
+ ))} +
+ )} +
+ ))} +
+ + {/* Submit */} +
+ + +
+
+ + {/* Hint box */} +
+ +

+ {mode === 'stress' + ? 'These are 30-second window features from accelerometer, gyroscope, and microphone sensors. Defaults represent a calm driving scenario.' + : 'Enter your current shift stats. The model predicts your earnings velocity (₹/hr) and whether you\'re on track to hit your target.' + } +

+
+
+ + {/* Result panel — right 2 cols */} +
+ {error && ( +
+ +

{error}

+
+ )} + + {!result && !error && ( +
+ +

+ Fill in values and hit "Run Prediction" +

+

+ Results will appear here +

+
+ )} + + {result && mode === 'stress' && } + {result && mode === 'earnings' && } +
+
+ )} +
+ ) +} + + +/* ─── Stress result card ────────────────────────────────── */ +function StressResult({ result }) { + const sev = result.severity || 'low' + const conf = result.confidence || 0 + const confPct = (conf * 100).toFixed(1) + + // Build probability chart data + const probaData = result.all_probabilities + ? Object.entries(result.all_probabilities).map(([name, val]) => ({ + name: name.replace(/_/g, ' '), + value: +(val * 100).toFixed(1), + fill: SITUATION_COLORS[name] || '#999', + })) + : [] + + return ( +
+ {/* Main result */} +
+
+ {result.emoji} +
+

+ {result.situation_name?.replace(/_/g, ' ')} +

+

+ {sev.toUpperCase()} severity +

+
+
+ + {/* Confidence bar */} +
+
+ Confidence + {confPct}% +
+
+
= 0.85 ? 'bg-green-500' : conf >= 0.65 ? 'bg-yellow-500' : 'bg-red-400' + }`} + style={{ width: `${confPct}%` }} + /> +
+
+ + {/* Flags */} +
+ {result.should_notify && ( + + 🔔 Notify + + )} + {result.is_safety_critical && ( + + 🚨 Safety + + )} + {!result.should_notify && !result.is_safety_critical && ( + + No alerts + + )} +
+
+ + {/* Top features */} + {result.top_features?.length > 0 && ( +
+

Top Contributing Features

+
+ {result.top_features.map((f, i) => ( +
+ {f.feature} +
+ z={f.z_score} + {f.value} +
+
+ ))} +
+
+ )} + + {/* Probability distribution */} + {probaData.length > 0 && ( +
+

Class Probabilities

+ + + + `${v}%`} fontSize={11} /> + + `${v}%`} /> + + {probaData.map((entry, i) => ( + + ))} + + + +
+ )} +
+ ) +} + + +/* ─── Earnings result card ──────────────────────────────── */ +function EarningsResult({ result }) { + if (result.error) { + return ( +
+

{result.error}

+
+ ) + } + + const vel = result.predicted_velocity + const forecast = result.forecast_status || 'on_track' + const fBg = FORECAST_BG[forecast] || FORECAST_BG.on_track + const fText = FORECAST_TEXT[forecast] || FORECAST_TEXT.on_track + + return ( +
+ {/* Main result */} +
+
+ +
+

+ ₹{vel?.toFixed(2)}/hr +

+

+ {forecast.replace(/_/g, ' ').toUpperCase()} +

+
+
+ + {/* Stats */} +
+ {result.target_velocity != null && ( + + )} + {result.current_velocity != null && ( + + )} + {result.remaining_earnings != null && ( + + )} + {result.hours_to_target != null && ( + + )} +
+
+ + {/* Velocity comparison */} +
+

Velocity Comparison

+
+ + + {result.target_velocity != null && ( + + )} +
+
+
+ ) +} + +function Stat({ label, value }) { + return ( +
+

{label}

+

{value}

+
+ ) +} + +function VelBar({ label, value, max, color }) { + const pct = max > 0 ? Math.min((value / max) * 100, 100) : 0 + return ( +
+
+ {label} + ₹{value.toFixed(0)}/hr +
+
+
+
+
+ ) +} diff --git a/frontend/src/pages/Trends.jsx b/frontend/src/pages/Trends.jsx new file mode 100644 index 0000000..02ea263 --- /dev/null +++ b/frontend/src/pages/Trends.jsx @@ -0,0 +1,144 @@ +import { useState, useEffect } from 'react' +import { api } from '../api/client' +import { + BarChart, Bar, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, + ResponsiveContainer, Legend, AreaChart, Area, +} from 'recharts' +import { TrendingUp, TrendingDown, DollarSign, Car, AlertTriangle, Clock } from 'lucide-react' + +export default function Trends() { + const [range, setRange] = useState('7d') + const [metrics, setMetrics] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + setLoading(true) + api.getMetrics(range) + .then(setMetrics) + .catch(() => {}) + .finally(() => setLoading(false)) + }, [range]) + + if (loading) { + return ( +
+
+
+ ) + } + + if (!metrics) return null + + const { days, summary } = metrics + + const summaryCards = [ + { label: 'Avg Daily Earnings', value: `₹${summary.avg_daily_earnings?.toLocaleString()}`, icon: DollarSign, color: 'text-uber-green' }, + { label: 'Avg Daily Trips', value: summary.avg_daily_trips, icon: Car, color: 'text-uber-blue' }, + { label: 'Avg Stress Score', value: summary.avg_stress, icon: AlertTriangle, color: 'text-uber-red' }, + { label: 'Total Earnings', value: `₹${summary.total_earnings?.toLocaleString()}`, icon: TrendingUp, color: 'text-uber-orange' }, + ] + + return ( +
+ {/* Header + range toggle */} +
+

Personal Trends

+
+ {[{ key: '7d', label: 'Week' }, { key: '30d', label: 'Month' }].map(r => ( + + ))} +
+
+ + {/* Summary cards */} +
+ {summaryCards.map(({ label, value, icon: Icon, color }) => ( +
+ +

{value}

+

{label}

+
+ ))} +
+ + {/* Earnings chart */} +
+

Earnings Trend

+ + + + + + + + + + + `₹${v}`} /> + [`₹${v}`, 'Earnings']} /> + + + +
+ + {/* Stress + Trips chart */} +
+
+

Average Stress

+ + + + + + + + + +
+ +
+

Trips Count

+ + + + + + + + + +
+
+ + {/* Velocity chart */} +
+

Earnings Velocity (₹/hr)

+ + + + + + + + + + + `₹${v}`} /> + [`₹${v}`, 'Velocity']} /> + + + +
+
+ ) +} diff --git a/frontend/src/pages/TripDetail.jsx b/frontend/src/pages/TripDetail.jsx new file mode 100644 index 0000000..9da4940 --- /dev/null +++ b/frontend/src/pages/TripDetail.jsx @@ -0,0 +1,216 @@ +import { useState, useEffect, useRef } from 'react' +import { useParams, Link } from 'react-router-dom' +import { api } from '../api/client' +import TripMap from '../components/TripMap' +import SignalCharts from '../components/SignalCharts' +import TimelineSlider from '../components/TimelineSlider' +import EventCard from '../components/EventCard' +import ConfidenceBadge from '../components/ConfidenceBadge' +import { ArrowLeft, Download, Clock, DollarSign, MapPin, Activity } from 'lucide-react' + +export default function TripDetail() { + const { tripId } = useParams() + const [trip, setTrip] = useState(null) + const [loading, setLoading] = useState(true) + const [currentSec, setCurrentSec] = useState(0) + const [isPlaying, setIsPlaying] = useState(false) + const playRef = useRef(null) + + useEffect(() => { + api.getTrip(tripId) + .then(setTrip) + .catch(() => {}) + .finally(() => setLoading(false)) + }, [tripId]) + + // Playback + useEffect(() => { + if (isPlaying && trip) { + playRef.current = setInterval(() => { + setCurrentSec(prev => { + const next = prev + 10 + if (next >= trip.duration_min * 60) { + setIsPlaying(false) + return trip.duration_min * 60 + } + return next + }) + }, 500) + } + return () => clearInterval(playRef.current) + }, [isPlaying, trip]) + + const handlePlayPause = () => { + if (currentSec >= (trip?.duration_min || 0) * 60) setCurrentSec(0) + setIsPlaying(!isPlaying) + } + + const jumpTo = (sec) => { + setCurrentSec(sec) + setIsPlaying(false) + } + + const handleExport = (format) => { + if (!trip) return + let content, filename, mime + if (format === 'json') { + content = JSON.stringify(trip, null, 2) + filename = `${trip.id}.json` + mime = 'application/json' + } else { + // CSV of signals + const rows = trip.signals.timestamps.map((t, i) => + [t, trip.signals.speed[i], trip.signals.accel_magnitude[i], trip.signals.audio_db[i]].join(',') + ) + content = 'timestamp,speed,accel_magnitude,audio_db\n' + rows.join('\n') + filename = `${trip.id}_signals.csv` + mime = 'text/csv' + } + const blob = new Blob([content], { type: mime }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + a.click() + URL.revokeObjectURL(url) + } + + if (loading) { + return ( +
+
+
+ ) + } + + if (!trip) { + return ( +
+

Trip not found

+ Back to trips +
+ ) + } + + const maxSec = trip.duration_min * 60 + const cursorIndex = trip.route + ? Math.min(Math.floor((currentSec / maxSec) * trip.route.length), trip.route.length - 1) + : 0 + + return ( +
+ {/* Header */} +
+
+ + + +
+

Trip {trip.id}

+

+ {new Date(trip.start_time).toLocaleDateString()} ·{' '} + {new Date(trip.start_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} –{' '} + {new Date(trip.end_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +

+
+
+ + {/* Export */} +
+ + +
+
+ + {/* Stats row */} +
+
+ +

{trip.duration_min} min

+

Duration

+
+
+ +

{trip.distance_km} km

+

Distance

+
+
+ +

₹{trip.fare}

+

+ Fare {trip.surge_multiplier > 1 && `(${trip.surge_multiplier}× surge)`} +

+
+
+ +

6 ? 'text-uber-red' : + trip.stress_score > 3 ? 'text-uber-yellow' : 'text-uber-green' + }`}>{trip.stress_score}/10

+

Stress Score

+
+
+

{trip.events_count}

+

Events

+
+
+ + {/* Map */} +
+ +
+ + {/* Playback slider */} + + + {/* Signal charts + Events side by side */} +
+
+

Sensor Signals

+ +
+
+

+ Detected Events ({trip.events.length}) +

+
+ {trip.events.map(ev => ( + { + setTrip(prev => ({ + ...prev, + events: prev.events.map(e => + e.id === id ? { ...e, feedback: { label } } : e + ), + })) + }} + /> + ))} + {trip.events.length === 0 && ( +

No events detected

+ )} +
+
+
+
+ ) +} diff --git a/frontend/src/pages/Trips.jsx b/frontend/src/pages/Trips.jsx new file mode 100644 index 0000000..568f69e --- /dev/null +++ b/frontend/src/pages/Trips.jsx @@ -0,0 +1,221 @@ +import { useState, useEffect } from 'react' +import { api } from '../api/client' +import TripListItem from '../components/TripListItem' +import FilterChips from '../components/FilterChips' +import { Search, Calendar, SlidersHorizontal } from 'lucide-react' + +export default function Trips() { + const [trips, setTrips] = useState([]) + const [loading, setLoading] = useState(true) + const [date, setDate] = useState('2026-03-08') + const [preset, setPreset] = useState(null) + const [showFilters, setShowFilters] = useState(false) + const [filters, setFilters] = useState({ + stress: '', + earnings_min: '', + earnings_max: '', + duration_min: '', + duration_max: '', + time_of_day: '', + }) + const [confidenceFilter, setConfidenceFilter] = useState('') + + useEffect(() => { + loadTrips() + }, [date, preset]) + + const loadTrips = async () => { + setLoading(true) + try { + const params = { date } + if (preset) params.preset = preset + if (filters.stress) params.stress = filters.stress + if (filters.earnings_min) params.earnings_min = filters.earnings_min + if (filters.earnings_max) params.earnings_max = filters.earnings_max + if (filters.duration_min) params.duration_min = filters.duration_min + if (filters.duration_max) params.duration_max = filters.duration_max + if (filters.time_of_day) params.time_of_day = filters.time_of_day + + const res = await api.getTrips(params) + setTrips(res.trips) + } catch (err) { + console.error('Failed to load trips:', err) + } finally { + setLoading(false) + } + } + + const applyFilters = () => { + setPreset(null) + loadTrips() + setShowFilters(false) + } + + const clearFilters = () => { + setFilters({ stress: '', earnings_min: '', earnings_max: '', duration_min: '', duration_max: '', time_of_day: '' }) + setPreset(null) + setConfidenceFilter('') + } + + // Client-side confidence filtering on events + let displayTrips = trips + if (confidenceFilter) { + displayTrips = trips.filter(t => { + if (!t.events_summary) return true + return t.events_summary.some(e => { + // We don't have confidence in summary, use stress_level as proxy + if (confidenceFilter === 'high') return t.stress_level === 'high' + if (confidenceFilter === 'medium') return t.stress_level === 'medium' + if (confidenceFilter === 'low') return t.stress_level === 'low' + return true + }) + }) + } + + return ( +
+ {/* Header */} +
+

Trip History

+
+
+ + setDate(e.target.value)} + className="text-sm outline-none bg-transparent" + /> +
+ +
+
+ + {/* Quick filter presets */} + { setPreset(p); clearFilters() }} /> + + {/* Advanced filters panel */} + {showFilters && ( +
+

Advanced Filters

+
+
+ + +
+
+ + +
+
+ + +
+
+ + setFilters(f => ({ ...f, earnings_min: e.target.value }))} + className="w-full border border-uber-gray-200 rounded-lg px-3 py-2 text-sm outline-none" + placeholder="0" + /> +
+
+ + setFilters(f => ({ ...f, earnings_max: e.target.value }))} + className="w-full border border-uber-gray-200 rounded-lg px-3 py-2 text-sm outline-none" + placeholder="∞" + /> +
+
+ + setFilters(f => ({ ...f, duration_max: e.target.value }))} + className="w-full border border-uber-gray-200 rounded-lg px-3 py-2 text-sm outline-none" + placeholder="∞" + /> +
+
+
+ + +
+
+ )} + + {/* Results count */} +

+ {displayTrips.length} trip{displayTrips.length !== 1 ? 's' : ''} found +

+ + {/* Trip list */} + {loading ? ( +
+
+
+ ) : displayTrips.length === 0 ? ( +
+

No trips found

+

Try a different date or adjust your filters

+
+ ) : ( +
+ {displayTrips.map(trip => ( + + ))} +
+ )} +
+ ) +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..5c3431a --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,32 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,jsx}'], + theme: { + extend: { + colors: { + uber: { + black: '#000000', + white: '#FFFFFF', + green: '#06C167', + blue: '#276EF1', + red: '#E11900', + orange: '#FF6937', + yellow: '#FFC043', + gray: { + 50: '#F6F6F6', + 100: '#EEEEEE', + 200: '#E2E2E2', + 300: '#CBCBCB', + 400: '#AFAFAF', + 500: '#757575', + 600: '#545454', + 700: '#333333', + 800: '#1F1F1F', + 900: '#141414', + }, + }, + }, + }, + }, + plugins: [], +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..1406734 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + }, + }, +}) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2007904 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "Driver-Pulse", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From 98263f89ad6ea442b42ffbb312c597f5db38c315 Mon Sep 17 00:00:00 2001 From: Merin Theres Jose Date: Mon, 9 Mar 2026 00:13:54 +0530 Subject: [PATCH 03/13] Add README --- README.md | 67 ++++++++++++++++++++++++++++++++++++++++ backend/requirements.txt | 4 --- requirements.txt | 5 +++ 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 README.md delete mode 100644 backend/requirements.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd3c6fe --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# 🚗 DrivePulse + +Real-time driver wellness & earnings intelligence platform for ride-hailing drivers. Uses on-device sensor data (accelerometer, gyroscope, microphone) with ML models to detect stressful driving situations and forecast earnings velocity. + +--- + +## Features + +- **Dashboard** — Daily trips, earnings, stress score, timeline +- **Trip Detail** — Map playback, sensor charts, event detection with explainability +- **Trends** — Weekly/monthly earnings, stress, and velocity charts +- **Goals** — Set and track daily earnings targets +- **Manual Predict** — Enter sensor/earnings values → instant ML prediction +- **Batch Upload** — Upload CSV → run inference on multiple trips at once +- **Explainability** — Per-event feature contributions, confidence badges +- **Feedback** — Thumbs up/down on detected events + +--- + +## Architecture + +``` +Driver-Pulse/ +├── backend/ # FastAPI server +│ ├── main.py # API endpoints +│ └── data/ # Sample data + batch inference +├── frontend/ # React + Vite + Tailwind +│ └── src/ +│ ├── pages/ # Dashboard, Trips, TripDetail, Trends, Goals, Predict, BatchUpload +│ └── components/ # Sidebar, TripMap, SignalCharts, ExplainModal, etc. +├── drivepulse_stress_model/ # Stress ML pipeline +├── earnings/ # Earnings ML pipeline +└── requirements.txt +``` + +--- + +## Setup + +### Prerequisites +- Python 3.9+ +- Node.js 18+ + +### Install & Run + +```bash +# Install Python dependencies +pip install -r requirements.txt + +# Start backend (http://localhost:8000) +cd backend && python main.py + +# In a new terminal — start frontend (http://localhost:5173) +cd frontend && npm install && npm run dev +``` + +Open **http://localhost:5173** in your browser. + +--- + +## Tech Stack + +| Layer | Tech | +|-------|------| +| Frontend | React 18, Vite, Tailwind CSS, Recharts, Leaflet | +| Backend | FastAPI, Uvicorn | +| ML | scikit-learn, NumPy, Pandas | diff --git a/backend/requirements.txt b/backend/requirements.txt deleted file mode 100644 index b43a745..0000000 --- a/backend/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -fastapi>=0.104 -uvicorn>=0.24 -pydantic>=2.0 -python-dateutil>=2.8 diff --git a/requirements.txt b/requirements.txt index 6b9c98e..0d160ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,8 @@ numpy>=1.24.0 scikit-learn>=1.3.0 joblib>=1.3.0 streamlit>=1.28.0 +fastapi>=0.104 +uvicorn>=0.24 +pydantic>=2.0 +python-dateutil>=2.8 +python-multipart>=0.0.6 From 9d1fdf107899695ec5286976c9c79026148cf038 Mon Sep 17 00:00:00 2001 From: Arjun Date: Mon, 9 Mar 2026 12:39:18 +0530 Subject: [PATCH 04/13] add login and register and bug fixes --- backend/data/batch_processor.py | 30 ++- backend/data/sample_data.py | 33 +++ backend/data/users.py | 146 ++++++++++ backend/main.py | 92 +++++-- backend/requirements.txt | 9 + frontend/src/App.jsx | 43 ++- frontend/src/api/client.js | 15 ++ frontend/src/components/Layout.jsx | 4 +- frontend/src/components/Sidebar.jsx | 48 +++- frontend/src/pages/BatchUpload.jsx | 4 +- frontend/src/pages/Home.jsx | 397 ++++++++++++++++++++++++++++ 11 files changed, 786 insertions(+), 35 deletions(-) create mode 100644 backend/data/users.py create mode 100644 backend/requirements.txt create mode 100644 frontend/src/pages/Home.jsx diff --git a/backend/data/batch_processor.py b/backend/data/batch_processor.py index 47a42e9..ab43d41 100644 --- a/backend/data/batch_processor.py +++ b/backend/data/batch_processor.py @@ -282,6 +282,22 @@ def process_stress_csv(csv_content: str) -> dict: situation_counts = {} for i, row in enumerate(rows): + # Add constant features + row["motion_std"] = 0.3 + row["z_dev_max"] = 0.5 + row["spikes_above3"] = 0 + row["spikes_above5"] = 0 + row["audio_class_max"] = 2.0 + row["audio_class_mean"] = 1.0 + row["sustained_max"] = 10.0 + row["sustained_sum"] = 50.0 + row["cadence_var_max"] = 0.6 + row["audio_leads_motion"] = 0.0 + row["audio_onset_sec"] = 0.0 + row["brake_t_sec"] = 0.0 + row["is_low_speed"] = 0 + row["both_elevated"] = 0 + row["audio_only"] = 0 pred = predict_stress_row(row) pred["row_index"] = i # Carry through any extra columns (trip_id, timestamp, etc.) @@ -384,20 +400,14 @@ def process_earnings_csv(csv_content: str) -> dict: def stress_csv_template() -> str: """Return a CSV template string with headers + 2 sample rows.""" feats = [ - "motion_max", "motion_mean", "motion_p95", "motion_std", - "brake_intensity", "lateral_max", "z_dev_max", + "motion_max", "motion_mean", "motion_p95", "brake_intensity", "lateral_max", "speed_mean", "speed_at_brake", "speed_drop", - "spikes_above3", "spikes_above5", "audio_db_max", "audio_db_mean", "audio_db_p90", "audio_db_std", - "audio_class_max", "audio_class_mean", "sustained_max", "sustained_sum", - "cadence_var_mean", "cadence_var_max", - "argument_frac", "loud_frac", - "audio_leads_motion", "audio_onset_sec", "brake_t_sec", - "is_low_speed", "both_elevated", "audio_only", + "cadence_var_mean", "argument_frac", "loud_frac", ] header = "trip_id,timestamp," + ",".join(feats) - row1 = "trip-001,08:15:00,1.2,0.6,1.1,0.3,0.8,0.5,0.2,35,35,5,0,0,68,62,67,4,2,1.2,0,0,0.1,0.15,0.0,0.05,0,15,15,0,0,0" - row2 = "trip-002,09:30:00,5.1,1.8,4.8,1.4,4.9,2.1,0.4,40,40,25,4,2,94,82,91,8,5,3.8,45,180,0.72,0.95,0.55,0.82,-1.5,12,14,0,1,0" + row1 = "trip-001,08:15:00,1.2,0.6,1.1,0.8,0.5,35,35,5,68,62,67,4,0.1,0.15" + row2 = "trip-002,09:30:00,5.1,1.8,4.8,4.9,2.1,40,40,25,94,82,91,8,0.72,0.95" return header + "\n" + row1 + "\n" + row2 + "\n" diff --git a/backend/data/sample_data.py b/backend/data/sample_data.py index dd19539..e95fb75 100644 --- a/backend/data/sample_data.py +++ b/backend/data/sample_data.py @@ -414,5 +414,38 @@ def set_goal_target(target: float): global _GOALS goals = get_goals() goals["daily_target"] = target + + # Recalculate dependent values + remaining_earnings = max(0, target - goals["current_earnings"]) + remaining_hours = max(0.1, goals["target_hours"] - goals["current_hours"]) + goals["required_velocity"] = round(remaining_earnings / remaining_hours, 0) + + # Simple probability calculation based on current velocity vs required + velocity_ratio = goals["current_velocity"] / max(goals["required_velocity"], 1) + time_ratio = goals["current_hours"] / goals["target_hours"] + + if velocity_ratio >= 1.2: + prob = 0.95 + elif velocity_ratio >= 1.0: + prob = 0.75 + elif velocity_ratio >= 0.8: + prob = 0.55 + else: + prob = 0.25 + + # Adjust based on time progress + if time_ratio > 0.8: + prob *= 0.8 # Less time left, harder to catch up + + goals["goal_probability"] = round(min(0.99, max(0.01, prob)), 2) + + # Update forecast status + if goals["current_velocity"] >= goals["required_velocity"] * 1.1: + goals["forecast_status"] = "ahead" + elif goals["current_velocity"] >= goals["required_velocity"] * 0.9: + goals["forecast_status"] = "on_track" + else: + goals["forecast_status"] = "at_risk" + _GOALS = goals return _GOALS diff --git a/backend/data/users.py b/backend/data/users.py new file mode 100644 index 0000000..f6eaac2 --- /dev/null +++ b/backend/data/users.py @@ -0,0 +1,146 @@ +""" +DrivePulse Backend — User authentication and driver profiles. +Manages login, registration, and driver data. +""" + +import json +from datetime import datetime +from typing import Optional, Dict + +# In-memory user store (in production, use a real database) +_USERS = { + "alex.kumar": { + "driver_id": "DRV001", + "password": "password123", # In production, use bcrypt + "name": "Alex Kumar", + "email": "alex.kumar@example.com", + "phone": "+91 9876543210", + "city": "Mumbai", + "vehicle_type": "Sedan", + "vehicle_number": "MH01AB1234", + "shift_preference": "morning", + "avg_hours_per_day": 7.5, + "avg_earnings_per_hour": 185, + "experience_months": 18, + "rating": 4.8, + "total_trips": 342, + "total_earnings": 48000, + }, + "priya.singh": { + "driver_id": "DRV002", + "password": "password123", + "name": "Priya Singh", + "email": "priya.singh@example.com", + "phone": "+91 9876543211", + "city": "Bangalore", + "vehicle_type": "SUV", + "vehicle_number": "KA01CD5678", + "shift_preference": "evening", + "avg_hours_per_day": 8.2, + "avg_earnings_per_hour": 195, + "experience_months": 24, + "rating": 4.9, + "total_trips": 456, + "total_earnings": 62000, + }, +} + + +def register_user(username: str, password: str, driver_data: dict) -> Optional[Dict]: + """Register a new user with driver profile.""" + if username in _USERS: + return None # User already exists + + user = { + "driver_id": driver_data.get("driver_id", f"DRV{len(_USERS) + 1000}"), + "password": password, # In production, use bcrypt.hashpw() + "name": driver_data.get("name", ""), + "email": driver_data.get("email", ""), + "phone": driver_data.get("phone", ""), + "city": driver_data.get("city", ""), + "vehicle_type": driver_data.get("vehicle_type", ""), + "vehicle_number": driver_data.get("vehicle_number", ""), + "shift_preference": driver_data.get("shift_preference", "morning"), + "avg_hours_per_day": driver_data.get("avg_hours_per_day", 7.0), + "avg_earnings_per_hour": driver_data.get("avg_earnings_per_hour", 180), + "experience_months": driver_data.get("experience_months", 0), + "rating": driver_data.get("rating", 4.5), + "total_trips": driver_data.get("total_trips", 0), + "total_earnings": driver_data.get("total_earnings", 0), + } + + _USERS[username] = user + return { + "driver_id": user["driver_id"], + "name": user["name"], + "email": user["email"], + "city": user["city"], + "rating": user["rating"], + "vehicle_type": user["vehicle_type"], + } + + +def login_user(username: str, password: str) -> Optional[Dict]: + """Authenticate a user and return their profile.""" + if username not in _USERS: + return None + + user = _USERS[username] + + # Simple password check (in production, use bcrypt.checkpw()) + if user["password"] != password: + return None + + return { + "driver_id": user["driver_id"], + "username": username, + "name": user["name"], + "email": user["email"], + "phone": user["phone"], + "city": user["city"], + "vehicle_type": user["vehicle_type"], + "vehicle_number": user["vehicle_number"], + "rating": user["rating"], + "total_trips": user["total_trips"], + "total_earnings": user["total_earnings"], + "avg_hours_per_day": user["avg_hours_per_day"], + "avg_earnings_per_hour": user["avg_earnings_per_hour"], + "experience_months": user["experience_months"], + "shift_preference": user["shift_preference"], + } + + +def get_user_profile(driver_id: str) -> Optional[Dict]: + """Get user profile by driver ID.""" + for user in _USERS.values(): + if user["driver_id"] == driver_id: + return { + "driver_id": user["driver_id"], + "name": user["name"], + "email": user["email"], + "phone": user["phone"], + "city": user["city"], + "vehicle_type": user["vehicle_type"], + "vehicle_number": user["vehicle_number"], + "rating": user["rating"], + "total_trips": user["total_trips"], + "total_earnings": user["total_earnings"], + "avg_hours_per_day": user["avg_hours_per_day"], + "avg_earnings_per_hour": user["avg_earnings_per_hour"], + "experience_months": user["experience_months"], + "shift_preference": user["shift_preference"], + } + return None + + +def list_all_users() -> list: + """List all available users for demo purposes.""" + return [ + { + "username": username, + "name": user["name"], + "city": user["city"], + "rating": user["rating"], + } + for username, user in _USERS.items() + ] diff --git a/backend/main.py b/backend/main.py index e7de4d2..6a9107f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -20,6 +20,9 @@ stress_csv_template, earnings_csv_template, predict_stress_row, predict_earnings_row, ) +from data.users import ( + login_user, register_user, get_user_profile, list_all_users, +) app = FastAPI(title="DrivePulse API", version="1.0.0") @@ -45,6 +48,26 @@ class GoalPayload(BaseModel): daily_target: float +class LoginPayload(BaseModel): + username: str + password: str + + +class RegisterPayload(BaseModel): + username: str + password: str + name: str + email: str + phone: str + city: str + vehicle_type: str + vehicle_number: str + shift_preference: str = "morning" + avg_hours_per_day: float = 7.0 + avg_earnings_per_hour: float = 180 + experience_months: int = 0 + + # ── Routes ──────────────────────────────────────────────────────────────── @app.get("/api/health") @@ -52,6 +75,44 @@ def health(): return {"status": "ok", "timestamp": datetime.now().isoformat()} +# ── Authentication ─────────────────────────────────────────────────────── + +@app.post("/api/auth/login") +def login(payload: LoginPayload): + """Authenticate a driver and return their profile.""" + user = login_user(payload.username, payload.password) + if not user: + raise HTTPException(401, "Invalid username or password") + return user + + +@app.post("/api/auth/register") +def register(payload: RegisterPayload): + """Register a new driver account.""" + driver_data = { + "name": payload.name, + "email": payload.email, + "phone": payload.phone, + "city": payload.city, + "vehicle_type": payload.vehicle_type, + "vehicle_number": payload.vehicle_number, + "shift_preference": payload.shift_preference, + "avg_hours_per_day": payload.avg_hours_per_day, + "avg_earnings_per_hour": payload.avg_earnings_per_hour, + "experience_months": payload.experience_months, + } + result = register_user(payload.username, payload.password, driver_data) + if not result: + raise HTTPException(400, "Username already exists") + return result + + +@app.get("/api/auth/users") +def list_users(): + """List all available demo users.""" + return list_all_users() + + @app.get("/api/profile") def profile(): return get_profile() @@ -258,6 +319,22 @@ def earnings_template(): def predict_stress(payload: dict): """Submit a single row of sensor features → get stress prediction.""" try: + # Add constant features + payload.setdefault("motion_std", 0.3) + payload.setdefault("z_dev_max", 0.5) + payload.setdefault("spikes_above3", 0) + payload.setdefault("spikes_above5", 0) + payload.setdefault("audio_class_max", 2.0) + payload.setdefault("audio_class_mean", 1.0) + payload.setdefault("sustained_max", 10.0) + payload.setdefault("sustained_sum", 50.0) + payload.setdefault("cadence_var_max", 0.6) + payload.setdefault("audio_leads_motion", 0.0) + payload.setdefault("audio_onset_sec", 0.0) + payload.setdefault("brake_t_sec", 0.0) + payload.setdefault("is_low_speed", 0) + payload.setdefault("both_elevated", 0) + payload.setdefault("audio_only", 0) result = predict_stress_row(payload) return result except Exception as e: @@ -281,33 +358,18 @@ def stress_features(): {"name": "motion_max", "label": "Motion Max (g)", "default": 1.5, "group": "Motion"}, {"name": "motion_mean", "label": "Motion Mean (g)", "default": 0.8, "group": "Motion"}, {"name": "motion_p95", "label": "Motion P95 (g)", "default": 1.2, "group": "Motion"}, - {"name": "motion_std", "label": "Motion Std (g)", "default": 0.3, "group": "Motion"}, {"name": "brake_intensity", "label": "Brake Intensity", "default": 0.5, "group": "Motion"}, {"name": "lateral_max", "label": "Lateral Max (g)", "default": 0.8, "group": "Motion"}, - {"name": "z_dev_max", "label": "Z Deviation Max", "default": 0.5, "group": "Motion"}, {"name": "speed_mean", "label": "Speed Mean (km/h)", "default": 30.0, "group": "Speed"}, {"name": "speed_at_brake", "label": "Speed at Brake (km/h)", "default": 25.0, "group": "Speed"}, {"name": "speed_drop", "label": "Speed Drop (km/h)", "default": 5.0, "group": "Speed"}, - {"name": "spikes_above3", "label": "Spikes > 3g", "default": 0, "group": "Motion"}, - {"name": "spikes_above5", "label": "Spikes > 5g", "default": 0, "group": "Motion"}, {"name": "audio_db_max", "label": "Audio dB Max", "default": 65.0, "group": "Audio"}, {"name": "audio_db_mean", "label": "Audio dB Mean", "default": 55.0, "group": "Audio"}, {"name": "audio_db_p90", "label": "Audio dB P90", "default": 62.0, "group": "Audio"}, {"name": "audio_db_std", "label": "Audio dB Std", "default": 5.0, "group": "Audio"}, - {"name": "audio_class_max", "label": "Audio Class Max", "default": 2.0, "group": "Audio"}, - {"name": "audio_class_mean", "label": "Audio Class Mean", "default": 1.0, "group": "Audio"}, - {"name": "sustained_max", "label": "Sustained Max", "default": 10.0, "group": "Audio"}, - {"name": "sustained_sum", "label": "Sustained Sum", "default": 50.0, "group": "Audio"}, {"name": "cadence_var_mean", "label": "Cadence Var Mean", "default": 0.3, "group": "Voice"}, - {"name": "cadence_var_max", "label": "Cadence Var Max", "default": 0.6, "group": "Voice"}, {"name": "argument_frac", "label": "Argument Fraction", "default": 0.0, "group": "Voice"}, {"name": "loud_frac", "label": "Loud Fraction", "default": 0.1, "group": "Voice"}, - {"name": "audio_leads_motion", "label": "Audio Leads Motion (s)", "default": 0.0, "group": "Timing"}, - {"name": "audio_onset_sec", "label": "Audio Onset (s)", "default": 15.0, "group": "Timing"}, - {"name": "brake_t_sec", "label": "Brake Time (s)", "default": 15.0, "group": "Timing"}, - {"name": "is_low_speed", "label": "Is Low Speed (0/1)", "default": 0, "group": "Flags"}, - {"name": "both_elevated", "label": "Both Elevated (0/1)", "default": 0, "group": "Flags"}, - {"name": "audio_only", "label": "Audio Only (0/1)", "default": 0, "group": "Flags"}, ] return {"features": features, "total": len(features)} diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..fcec658 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,9 @@ +fastapi>=0.104 +uvicorn>=0.24 +pydantic>=2.0 +python-multipart>=0.0.6 +numpy>=1.24.0 +pandas>=2.0.0 +scikit-learn>=1.3.0 +joblib>=1.3.0 +python-dateutil>=2.8 \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index fd43534..d9d41b4 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,4 +1,5 @@ -import { Routes, Route } from 'react-router-dom' +import { Routes, Route, Navigate } from 'react-router-dom' +import { useState, useEffect } from 'react' import Layout from './components/Layout' import Dashboard from './pages/Dashboard' import Trips from './pages/Trips' @@ -7,11 +8,49 @@ import Trends from './pages/Trends' import Goals from './pages/Goals' import BatchUpload from './pages/BatchUpload' import Predict from './pages/Predict' +import Home from './pages/Home' export default function App() { + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + // Check if user is already logged in + const storedUser = localStorage.getItem('user') + if (storedUser) { + try { + setUser(JSON.parse(storedUser)) + } catch (e) { + localStorage.removeItem('user') + } + } + setLoading(false) + }, []) + + const handleLoginSuccess = (userData) => { + setUser(userData) + } + + const handleLogout = () => { + setUser(null) + localStorage.removeItem('user') + } + + if (loading) { + return ( +
+
+
+ ) + } + + if (!user) { + return + } + return ( - }> + }> } /> } /> } /> diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index b650d5d..ebb0fa3 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -10,6 +10,21 @@ async function request(path, options = {}) { } export const api = { + // Authentication + login: (username, password) => + request('/auth/login', { + method: 'POST', + body: JSON.stringify({ username, password }), + }), + + register: (userData) => + request('/auth/register', { + method: 'POST', + body: JSON.stringify(userData), + }), + + listUsers: () => request('/auth/users'), + // Dashboard getDashboard: () => request('/dashboard'), getProfile: () => request('/profile'), diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 13f7771..db796d4 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -1,10 +1,10 @@ import { Outlet } from 'react-router-dom' import Sidebar from './Sidebar' -export default function Layout() { +export default function Layout({ user, onLogout }) { return (
- +
diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 0cbc193..a1ee69b 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -1,5 +1,5 @@ import { NavLink } from 'react-router-dom' -import { LayoutDashboard, MapPin, TrendingUp, Target, Activity, Upload, PenLine } from 'lucide-react' +import { LayoutDashboard, MapPin, TrendingUp, Target, Activity, Upload, PenLine, LogOut, User, Star, Truck } from 'lucide-react' const links = [ { to: '/', label: 'Dashboard', icon: LayoutDashboard }, @@ -10,7 +10,7 @@ const links = [ { to: '/batch', label: 'Batch Upload', icon: Upload }, ] -export default function Sidebar() { +export default function Sidebar({ user, onLogout }) { return (
+ {/* User Profile */} + {user && ( +
+
+
+ +
+
+

{user.name}

+

@{user.username}

+
+ + {user.rating} +
+
+
+
+
+ + {user.vehicle_type} +
+
+ 📍 + {user.city} +
+
+
+ )} + {/* Navigation */} + {/* Logout */} +
+ +
+ {/* Footer */} -
- DrivePulse v1.0 · Hackathon 2026 +
+ DrivePulse v1.0 · 2026
) diff --git a/frontend/src/pages/BatchUpload.jsx b/frontend/src/pages/BatchUpload.jsx index 88a3e5a..d53b067 100644 --- a/frontend/src/pages/BatchUpload.jsx +++ b/frontend/src/pages/BatchUpload.jsx @@ -123,7 +123,7 @@ export default function BatchUpload() {

{mode === 'stress' - ? 'Each row = one 30-second sensor window. Requires 30 feature columns (motion, audio, speed aggregates). Add optional trip_id and timestamp columns for identification.' + ? 'Each row = one 30-second sensor window. Requires 15 feature columns (motion, audio, speed aggregates). Add optional trip_id and timestamp columns for identification.' : 'Each row = one earnings velocity log entry. Requires driver_id, timestamp, cumulative_earnings, elapsed_hours, current_velocity, target_velocity, velocity_delta, trips_completed, target_earnings.' }

@@ -131,7 +131,7 @@ export default function BatchUpload() {

Required columns

{(mode === 'stress' - ? ['motion_max', 'motion_mean', 'motion_p95', 'brake_intensity', 'audio_db_max', 'audio_db_p90', 'speed_mean', '...+23 more'] + ? ['motion_max', 'motion_mean', 'motion_p95', 'brake_intensity', 'audio_db_max', 'audio_db_p90', 'speed_mean', '...+8 more'] : ['driver_id', 'timestamp', 'cumulative_earnings', 'elapsed_hours', 'current_velocity', 'target_velocity', 'trips_completed', 'target_earnings'] ).map(col => ( diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx new file mode 100644 index 0000000..8b364ac --- /dev/null +++ b/frontend/src/pages/Home.jsx @@ -0,0 +1,397 @@ +import { useState, useEffect } from 'react' +import { Mail, Lock, User, Phone, MapPin, Truck, Users, LogIn, UserPlus } from 'lucide-react' + +export default function Home({ onLoginSuccess }) { + const [mode, setMode] = useState('login') // 'login' | 'register' + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [demoUsers, setDemoUsers] = useState([]) + const [loadingUsers, setLoadingUsers] = useState(true) + + // Login form state + const [loginForm, setLoginForm] = useState({ username: '', password: '' }) + + // Register form state + const [registerForm, setRegisterForm] = useState({ + username: '', + password: '', + name: '', + email: '', + phone: '', + city: '', + vehicle_type: 'Sedan', + vehicle_number: '', + shift_preference: 'morning', + avg_hours_per_day: 7.0, + avg_earnings_per_hour: 180, + experience_months: 0, + }) + + // Load demo users + useEffect(() => { + fetch('/api/auth/users') + .then(r => r.json()) + .then(users => setDemoUsers(users)) + .catch(() => {}) + .finally(() => setLoadingUsers(false)) + }, []) + + const handleLogin = async (e) => { + e.preventDefault() + setError(null) + setLoading(true) + + try { + const res = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username: loginForm.username, + password: loginForm.password, + }), + }) + if (!res.ok) { + const text = await res.text() + throw new Error(text || 'Login failed') + } + const user = await res.json() + localStorage.setItem('user', JSON.stringify(user)) + onLoginSuccess(user) + } catch (err) { + setError(err.message || 'Login failed') + } finally { + setLoading(false) + } + } + + const handleRegister = async (e) => { + e.preventDefault() + setError(null) + setLoading(true) + + try { + const res = await fetch('/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(registerForm), + }) + if (!res.ok) { + const text = await res.text() + throw new Error(text || 'Registration failed') + } + const user = await res.json() + // Auto-login after registration + const loginRes = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username: registerForm.username, + password: registerForm.password, + }), + }) + const fullUser = await loginRes.json() + localStorage.setItem('user', JSON.stringify(fullUser)) + onLoginSuccess(fullUser) + } catch (err) { + setError(err.message || 'Registration failed') + } finally { + setLoading(false) + } + } + + const demoLogin = (username) => { + setLoginForm({ username, password: '' }) + setError(null) + } + + return ( +
+
+ {/* Header */} +
+

DrivePulse

+

Smart Stress Detection & Earnings Tracking

+
+ +
+ {/* Left side - Demo users */} +
+
+ +

Demo Users

+
+ + {loadingUsers ? ( +
+
+
+ ) : ( +
+ {demoUsers.map(user => ( + + ))} +
+ )} + +
+

💡 Tip

+

+ Click on any demo user above, then click Continue to login. +
+ Default password: password123 +

+
+
+ + {/* Right side - Login/Register form */} +
+ {/* Mode toggle */} +
+ {[ + { key: 'login', label: 'Login', icon: LogIn }, + { key: 'register', label: 'Register', icon: UserPlus }, + ].map(({ key, label, icon: Icon }) => ( + + ))} +
+ + {error && ( +
+ {error} +
+ )} + + {mode === 'login' ? ( +
+
+ + setLoginForm({...loginForm, username: e.target.value})} + className="w-full px-4 py-2 border border-uber-gray-200 rounded-lg focus:border-uber-blue focus:outline-none transition" + placeholder="e.g., alex.kumar" + required + /> +
+ +
+ + setLoginForm({...loginForm, password: e.target.value})} + className="w-full px-4 py-2 border border-uber-gray-200 rounded-lg focus:border-uber-blue focus:outline-none transition" + placeholder="••••••••" + required + /> +
+ + +
+ ) : ( +
+
+
+ + setRegisterForm({...registerForm, username: e.target.value})} + className="w-full px-3 py-2 border border-uber-gray-200 rounded-lg focus:border-uber-blue focus:outline-none transition text-sm" + placeholder="username" + required + /> +
+
+ + setRegisterForm({...registerForm, password: e.target.value})} + className="w-full px-3 py-2 border border-uber-gray-200 rounded-lg focus:border-uber-blue focus:outline-none transition text-sm" + placeholder="••••••••" + required + /> +
+
+ +
+ + setRegisterForm({...registerForm, name: e.target.value})} + className="w-full px-3 py-2 border border-uber-gray-200 rounded-lg focus:border-uber-blue focus:outline-none transition text-sm" + placeholder="John Doe" + required + /> +
+ +
+
+ + setRegisterForm({...registerForm, email: e.target.value})} + className="w-full px-3 py-2 border border-uber-gray-200 rounded-lg focus:border-uber-blue focus:outline-none transition text-sm" + placeholder="email@example.com" + required + /> +
+
+ + setRegisterForm({...registerForm, phone: e.target.value})} + className="w-full px-3 py-2 border border-uber-gray-200 rounded-lg focus:border-uber-blue focus:outline-none transition text-sm" + placeholder="+91 XXXXXXXXXX" + required + /> +
+
+ +
+
+ + setRegisterForm({...registerForm, city: e.target.value})} + className="w-full px-3 py-2 border border-uber-gray-200 rounded-lg focus:border-uber-blue focus:outline-none transition text-sm" + placeholder="Mumbai" + required + /> +
+
+ + +
+
+ +
+ + setRegisterForm({...registerForm, vehicle_number: e.target.value})} + className="w-full px-3 py-2 border border-uber-gray-200 rounded-lg focus:border-uber-blue focus:outline-none transition text-sm" + placeholder="MH01AB1234" + required + /> +
+ +
+
+ + +
+
+ + setRegisterForm({...registerForm, experience_months: parseInt(e.target.value)})} + className="w-full px-3 py-2 border border-uber-gray-200 rounded-lg focus:border-uber-blue focus:outline-none transition text-sm" + placeholder="0" + min="0" + /> +
+
+ + +
+ )} +
+
+ + {/* Footer */} +
+

Your stress detection and earnings companion 🚗✨

+
+
+
+ ) +} From 9de234389fb083e12e82782942f03d0793810a7c Mon Sep 17 00:00:00 2001 From: Arjun Date: Mon, 9 Mar 2026 12:46:54 +0530 Subject: [PATCH 05/13] update goals page --- frontend/src/components/EarningsProgress.jsx | 30 ++++++++++++++++++++ frontend/src/pages/Goals.jsx | 10 +++---- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/EarningsProgress.jsx b/frontend/src/components/EarningsProgress.jsx index d8ed3c0..faa8e5a 100644 --- a/frontend/src/components/EarningsProgress.jsx +++ b/frontend/src/components/EarningsProgress.jsx @@ -49,6 +49,36 @@ export default function EarningsProgress({ goals }) {

Probability

+ + {/* Details grid */} +
+
+

Remaining

+

₹{Math.max(0, goals.daily_target - goals.current_earnings).toLocaleString()}

+

to reach target

+
+
+

Time Worked

+

{goals.current_hours}h

+

of {goals.target_hours}h target

+
+
+

Trips Today

+

{goals.trips_completed}

+

completed

+
+
+

Status

+

+ {goals.forecast_status?.replace('_', ' ').charAt(0).toUpperCase() + goals.forecast_status?.replace('_', ' ').slice(1)} +

+

today's pace

+
+
) } diff --git a/frontend/src/pages/Goals.jsx b/frontend/src/pages/Goals.jsx index 8e3899f..b22b096 100644 --- a/frontend/src/pages/Goals.jsx +++ b/frontend/src/pages/Goals.jsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import { api } from '../api/client' import EarningsProgress from '../components/EarningsProgress' import StressTips from '../components/StressTips' -import { Target, Save, CheckCircle } from 'lucide-react' +import { Target, Save, CheckCircle, TrendingUp, Clock, DollarSign } from 'lucide-react' export default function Goals() { const [goals, setGoals] = useState(null) @@ -54,7 +54,7 @@ export default function Goals() {

Daily Earnings Target

-
+
-

+

Set your daily earnings target. Your progress will be tracked in real-time on the dashboard.

@@ -91,7 +91,7 @@ export default function Goals() { {/* Goal milestones */} - {goals && ( + {/* {goals && (

Today's Progress

@@ -118,7 +118,7 @@ export default function Goals() { })}
- )} + )} */} {/* Stress tips */} From 3cc5e2205dbc4408f049d76af392a852882a2a8d Mon Sep 17 00:00:00 2001 From: Arjun Date: Mon, 9 Mar 2026 13:33:36 +0530 Subject: [PATCH 06/13] implement add trips and similar features --- backend/data/sample_data.py | 124 ++++++++++++++++-- backend/data/trips_import.py | 112 ++++++++++++++++ backend/main.py | 86 +++++++++++++ frontend/src/api/client.js | 12 ++ frontend/src/pages/Trips.jsx | 241 ++++++++++++++++++++++++++++++++++- 5 files changed, 565 insertions(+), 10 deletions(-) create mode 100644 backend/data/trips_import.py diff --git a/backend/data/sample_data.py b/backend/data/sample_data.py index e95fb75..c01fade 100644 --- a/backend/data/sample_data.py +++ b/backend/data/sample_data.py @@ -11,6 +11,11 @@ random.seed(42) +# Canonical "today" used across demo endpoints. +# (The frontend defaults to this date as well.) +TODAY = datetime(2026, 3, 8) +TODAY_STR = TODAY.strftime("%Y-%m-%d") + # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- @@ -277,8 +282,7 @@ def generate_goals() -> Dict: def build_dashboard(trips: List[Dict], goals: Dict) -> Dict: """Build dashboard summary from trips and goals.""" - today_str = datetime(2026, 3, 8).strftime("%Y-%m-%d") - today_trips = [t for t in trips if t["date"] == today_str] + today_trips = [t for t in trips if t["date"] == TODAY_STR] total_earnings = sum(t["fare"] for t in today_trips) total_hours = round(sum(t["duration_min"] for t in today_trips) / 60, 1) @@ -287,7 +291,7 @@ def build_dashboard(trips: List[Dict], goals: Dict) -> Dict: pct_target = round(min(100, (total_earnings / goals["daily_target"]) * 100), 1) if goals["daily_target"] else 0 return { - "date": today_str, + "date": TODAY_STR, "total_trips": len(today_trips), "total_hours": total_hours, "total_earnings": total_earnings, @@ -301,7 +305,7 @@ def build_dashboard(trips: List[Dict], goals: Dict) -> Dict: def build_weekly_metrics(trips: List[Dict]) -> Dict: """Build aggregated metrics for trends page.""" - today = datetime(2026, 3, 8) + today = TODAY days = [] for i in range(6, -1, -1): day = today - timedelta(days=i) @@ -350,7 +354,7 @@ def build_weekly_metrics(trips: List[Dict]) -> Dict: def build_monthly_metrics() -> Dict: """Build 30-day metric trend.""" - today = datetime(2026, 3, 8) + today = TODAY days = [] for i in range(29, -1, -1): day = today - timedelta(days=i) @@ -396,6 +400,58 @@ def get_trips(): return _TRIPS +def create_user_trip( + *, + date: str, + start_time_iso: str, + end_time_iso: str, + duration_min: int, + distance_km: float, + fare: float, + stress_score: float = 0.0, +): + """Create a minimal, UI-compatible trip for manual entry/import flows.""" + # Keep detail page working by providing empty events + synthetic route/signals. + events = [] + route_anchors = ROUTE_ANCHORS[0] + route = _interp_route(route_anchors, n_points=max(30, duration_min)) + signals = _gen_signals(duration_min, events) + + stress_score = float(stress_score or 0.0) + # Derive stress_level for existing UI (dots/filters). + if stress_score <= 3: + stress_level = "low" + elif stress_score <= 6: + stress_level = "medium" + else: + stress_level = "high" + + return { + "id": f"user-{str(uuid.uuid4())[:8]}", + "date": date, + "start_time": start_time_iso, + "end_time": end_time_iso, + "duration_min": int(duration_min), + "distance_km": float(distance_km), + "fare": float(fare), + "surge_multiplier": 1.0, + "stress_score": round(min(10.0, max(0.0, stress_score)), 1), + "stress_level": stress_level, + "events_count": 0, + "events": events, + "route": route, + "pickup": route[0] if route else None, + "dropoff": route[-1] if route else None, + "signals": signals, + } + + +def add_trip(trip: Dict[str, Any]) -> Dict[str, Any]: + trips = get_trips() + trips.insert(0, trip) + return trip + + def get_profile(): global _PROFILE if _PROFILE is None: @@ -407,18 +463,70 @@ def get_goals(): global _GOALS if _GOALS is None: _GOALS = generate_goals() - return _GOALS + # Always derive "current" metrics from trips so UI stays consistent after add/import. + goals = _GOALS + trips = get_trips() + today_trips = [t for t in trips if t.get("date") == TODAY_STR] + current_earnings = round(sum(float(t.get("fare", 0) or 0) for t in today_trips), 2) + current_hours = round(sum(float(t.get("duration_min", 0) or 0) for t in today_trips) / 60, 2) + trips_completed = len(today_trips) + current_velocity = round(current_earnings / current_hours, 0) if current_hours > 0 else 0 + + goals["current_earnings"] = current_earnings + goals["current_hours"] = current_hours + goals["trips_completed"] = trips_completed + goals["current_velocity"] = current_velocity + + # Keep dependent values fresh (even if target was set earlier). + remaining_earnings = max(0, float(goals.get("daily_target") or 0) - current_earnings) + remaining_hours = max(0.1, float(goals.get("target_hours") or 0) - current_hours) + goals["required_velocity"] = round(remaining_earnings / remaining_hours, 0) if goals.get("daily_target") else 0 + goals["remaining_earnings"] = round(remaining_earnings, 2) + goals["remaining_hours"] = round(max(0.0, float(goals.get("target_hours") or 0) - current_hours), 2) + goals["hours_to_target"] = round(remaining_earnings / current_velocity, 2) if current_velocity > 0 else None + + # Recompute forecast probability + status based on current pace vs required pace. + velocity_ratio = goals["current_velocity"] / max(goals["required_velocity"], 1) + time_ratio = goals["current_hours"] / max(goals["target_hours"], 0.1) + + if velocity_ratio >= 1.2: + prob = 0.95 + elif velocity_ratio >= 1.0: + prob = 0.75 + elif velocity_ratio >= 0.8: + prob = 0.55 + else: + prob = 0.25 + + # Adjust based on time progress (less time left, harder to catch up). + if time_ratio > 0.8: + prob *= 0.8 + + goals["goal_probability"] = round(min(0.99, max(0.01, prob)), 2) + + if goals["current_velocity"] >= goals["required_velocity"] * 1.1: + goals["forecast_status"] = "ahead" + elif goals["current_velocity"] >= goals["required_velocity"] * 0.9: + goals["forecast_status"] = "on_track" + else: + goals["forecast_status"] = "at_risk" + + _GOALS = goals + return goals def set_goal_target(target: float): global _GOALS goals = get_goals() - goals["daily_target"] = target + goals["daily_target"] = float(target) # Recalculate dependent values - remaining_earnings = max(0, target - goals["current_earnings"]) + remaining_earnings = max(0, goals["daily_target"] - goals["current_earnings"]) remaining_hours = max(0.1, goals["target_hours"] - goals["current_hours"]) goals["required_velocity"] = round(remaining_earnings / remaining_hours, 0) + goals["remaining_earnings"] = round(remaining_earnings, 2) + goals["remaining_hours"] = round(max(0.0, goals["target_hours"] - goals["current_hours"]), 2) + goals["hours_to_target"] = round(remaining_earnings / goals["current_velocity"], 2) if goals["current_velocity"] > 0 else None # Simple probability calculation based on current velocity vs required velocity_ratio = goals["current_velocity"] / max(goals["required_velocity"], 1) diff --git a/backend/data/trips_import.py b/backend/data/trips_import.py new file mode 100644 index 0000000..b6b280d --- /dev/null +++ b/backend/data/trips_import.py @@ -0,0 +1,112 @@ +import csv +from datetime import datetime +from typing import Dict, Any, List, Optional, Tuple + +from .sample_data import create_user_trip, add_trip + + +REQUIRED_COLUMNS = ["date", "start_time", "end_time", "distance_km", "fare"] +OPTIONAL_COLUMNS = ["stress_score"] + + +def trips_csv_template() -> str: + header = REQUIRED_COLUMNS + OPTIONAL_COLUMNS + example_rows = [ + { + "date": "2026-03-08", + "start_time": "09:15", + "end_time": "09:45", + "distance_km": "8.2", + "fare": "310", + "stress_score": "2.5", + }, + { + "date": "2026-03-08", + "start_time": "18:05", + "end_time": "18:40", + "distance_km": "11.6", + "fare": "520", + "stress_score": "6.3", + }, + ] + lines = [",".join(header)] + for r in example_rows: + lines.append(",".join(str(r.get(k, "")) for k in header)) + return "\n".join(lines) + "\n" + + +def _parse_dt(date: str, dt_or_hhmm: str) -> datetime: + s = (dt_or_hhmm or "").strip() + if "T" in s: + return datetime.fromisoformat(s) + hh, mm = s.split(":") + return datetime.fromisoformat(f"{date}T{int(hh):02d}:{int(mm):02d}:00") + + +def import_trips_csv(csv_content: str) -> Dict[str, Any]: + """ + Parse a Trips CSV and append trips to the in-memory store. + Returns summary + per-row results (created trip IDs or errors). + """ + reader = csv.DictReader(csv_content.splitlines()) + if not reader.fieldnames: + return {"error": "CSV missing header row"} + + missing = [c for c in REQUIRED_COLUMNS if c not in reader.fieldnames] + if missing: + return {"error": f"Missing required columns: {', '.join(missing)}"} + + results: List[Dict[str, Any]] = [] + created: List[Dict[str, Any]] = [] + + for idx, row in enumerate(reader): + try: + date = (row.get("date") or "").strip() + if not date: + raise ValueError("date is required") + + start_dt = _parse_dt(date, row.get("start_time")) + end_dt = _parse_dt(date, row.get("end_time")) + if end_dt <= start_dt: + raise ValueError("end_time must be after start_time") + + duration_min = int(round((end_dt - start_dt).total_seconds() / 60)) + if duration_min <= 0: + raise ValueError("duration must be at least 1 minute") + + distance_km = float(row.get("distance_km")) + fare = float(row.get("fare")) + + stress_score_raw = (row.get("stress_score") or "").strip() + if stress_score_raw == "": + stress_score = 0.0 + else: + stress_score = float(stress_score_raw) + if stress_score < 0 or stress_score > 10: + raise ValueError("stress_score must be between 0 and 10") + + trip = create_user_trip( + date=date, + start_time_iso=start_dt.isoformat(), + end_time_iso=end_dt.isoformat(), + duration_min=duration_min, + distance_km=distance_km, + fare=fare, + stress_score=stress_score, + ) + add_trip(trip) + created.append(trip) + results.append({"row_index": idx, "status": "created", "trip_id": trip["id"]}) + except Exception as e: + results.append({"row_index": idx, "status": "error", "error": str(e)}) + + return { + "summary": { + "total_rows": len(results), + "created": sum(1 for r in results if r["status"] == "created"), + "errors": sum(1 for r in results if r["status"] == "error"), + }, + "results": results, + "created_trips": created, + } + diff --git a/backend/main.py b/backend/main.py index 6a9107f..cea8597 100644 --- a/backend/main.py +++ b/backend/main.py @@ -14,12 +14,14 @@ get_trips, get_profile, get_goals, set_goal_target, build_dashboard, build_weekly_metrics, build_monthly_metrics, STRESS_TIPS, SITUATIONS, + create_user_trip, add_trip, ) from data.batch_processor import ( process_stress_csv, process_earnings_csv, stress_csv_template, earnings_csv_template, predict_stress_row, predict_earnings_row, ) +from data.trips_import import import_trips_csv, trips_csv_template from data.users import ( login_user, register_user, get_user_profile, list_all_users, ) @@ -68,6 +70,15 @@ class RegisterPayload(BaseModel): experience_months: int = 0 +class TripCreatePayload(BaseModel): + date: str # YYYY-MM-DD + start_time: str # "HH:MM" (local) or ISO datetime + end_time: str # "HH:MM" (local) or ISO datetime + distance_km: float + fare: float + stress_score: Optional[float] = None # 0-10 (optional) + + # ── Routes ──────────────────────────────────────────────────────────────── @app.get("/api/health") @@ -129,6 +140,67 @@ def dashboard(): # ── Trips ───────────────────────────────────────────────────────────────── +@app.get("/api/trips/template", response_class=PlainTextResponse) +def trips_template(): + """Download a CSV template for trips import.""" + return PlainTextResponse( + content=trips_csv_template(), + media_type="text/csv", + headers={"Content-Disposition": "attachment; filename=trips_template.csv"}, + ) + + +@app.post("/api/trips") +def create_trip(payload: TripCreatePayload): + """Create a single trip (manual entry) and persist in the in-memory store.""" + try: + date = payload.date + + def _to_iso(dt_or_hhmm: str) -> datetime: + s = (dt_or_hhmm or "").strip() + # Accept ISO datetime if provided + if "T" in s: + return datetime.fromisoformat(s) + # Accept HH:MM, combine with date + hh, mm = s.split(":") + return datetime.fromisoformat(f"{date}T{int(hh):02d}:{int(mm):02d}:00") + + start_dt = _to_iso(payload.start_time) + end_dt = _to_iso(payload.end_time) + if end_dt <= start_dt: + raise HTTPException(400, "end_time must be after start_time") + + duration_min = int(round((end_dt - start_dt).total_seconds() / 60)) + if duration_min <= 0: + raise HTTPException(400, "Trip duration must be at least 1 minute") + + stress_score = payload.stress_score + if stress_score is None: + stress_score = 0.0 + try: + stress_score = float(stress_score) + except Exception: + raise HTTPException(400, "stress_score must be a number between 0 and 10") + if stress_score < 0 or stress_score > 10: + raise HTTPException(400, "stress_score must be between 0 and 10") + + trip = create_user_trip( + date=date, + start_time_iso=start_dt.isoformat(), + end_time_iso=end_dt.isoformat(), + duration_min=duration_min, + distance_km=payload.distance_km, + fare=payload.fare, + stress_score=stress_score, + ) + add_trip(trip) + return trip + except HTTPException: + raise + except Exception as e: + raise HTTPException(400, str(e)) + + @app.get("/api/trips") def list_trips( date: Optional[str] = Query(None, description="YYYY-MM-DD"), @@ -313,6 +385,20 @@ def earnings_template(): ) +# ── Trips CSV Import (creates trips) ─────────────────────────────────────── + +@app.post("/api/trips/import-csv") +async def import_trips(file: UploadFile = File(...)): + """Upload a Trips CSV and persist rows as trips (in-memory).""" + content = (await file.read()).decode("utf-8") + if not content.strip(): + raise HTTPException(400, "Empty file") + result = import_trips_csv(content) + if "error" in result and result["error"]: + raise HTTPException(400, result["error"]) + return result + + # ── Single-row manual prediction ────────────────────────────────────────── @app.post("/api/predict/stress") diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index ebb0fa3..ea0e927 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -40,6 +40,18 @@ export const api = { }, getTrip: (id) => request(`/trips/${id}`), getSampleTrip: () => request('/sample-trip'), + createTrip: (payload) => + request('/trips', { method: 'POST', body: JSON.stringify(payload) }), + importTripsCsv: async (file) => { + const formData = new FormData() + formData.append('file', file) + const res = await fetch(`${BASE}/trips/import-csv`, { method: 'POST', body: formData }) + if (!res.ok) { + const text = await res.text() + throw new Error(text || `Import failed (${res.status})`) + } + return res.json() + }, // Events getTripEvents: (tripId) => request(`/trips/${tripId}/events`), diff --git a/frontend/src/pages/Trips.jsx b/frontend/src/pages/Trips.jsx index 568f69e..3424f47 100644 --- a/frontend/src/pages/Trips.jsx +++ b/frontend/src/pages/Trips.jsx @@ -1,8 +1,8 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { api } from '../api/client' import TripListItem from '../components/TripListItem' import FilterChips from '../components/FilterChips' -import { Search, Calendar, SlidersHorizontal } from 'lucide-react' +import { Calendar, SlidersHorizontal, Plus, Upload, Download, X } from 'lucide-react' export default function Trips() { const [trips, setTrips] = useState([]) @@ -10,6 +10,22 @@ export default function Trips() { const [date, setDate] = useState('2026-03-08') const [preset, setPreset] = useState(null) const [showFilters, setShowFilters] = useState(false) + const [showAddTrip, setShowAddTrip] = useState(false) + const [creating, setCreating] = useState(false) + const [createError, setCreateError] = useState('') + const [newTrip, setNewTrip] = useState({ + date: '2026-03-08', + start_time: '', + end_time: '', + distance_km: '', + fare: '', + stress_score: '', + }) + + const [importing, setImporting] = useState(false) + const [importError, setImportError] = useState('') + const [importSummary, setImportSummary] = useState(null) + const importInputRef = useRef(null) const [filters, setFilters] = useState({ stress: '', earnings_min: '', @@ -24,6 +40,10 @@ export default function Trips() { loadTrips() }, [date, preset]) + useEffect(() => { + setNewTrip(t => ({ ...t, date })) + }, [date]) + const loadTrips = async () => { setLoading(true) try { @@ -45,6 +65,49 @@ export default function Trips() { } } + const submitNewTrip = async (e) => { + e.preventDefault() + setCreateError('') + setCreating(true) + try { + const payload = { + date: newTrip.date, + start_time: newTrip.start_time, + end_time: newTrip.end_time, + distance_km: Number(newTrip.distance_km), + fare: Number(newTrip.fare), + } + if (newTrip.stress_score !== '' && newTrip.stress_score !== null && newTrip.stress_score !== undefined) { + payload.stress_score = Number(newTrip.stress_score) + } + await api.createTrip(payload) + setShowAddTrip(false) + setNewTrip(t => ({ ...t, start_time: '', end_time: '', distance_km: '', fare: '', stress_score: '' })) + await loadTrips() + } catch (err) { + setCreateError(err?.message || 'Failed to add trip') + } finally { + setCreating(false) + } + } + + const handleImportTripsCsv = async (file) => { + if (!file) return + setImportError('') + setImportSummary(null) + setImporting(true) + try { + const res = await api.importTripsCsv(file) + setImportSummary(res.summary) + await loadTrips() + } catch (err) { + setImportError(err?.message || 'Import failed') + } finally { + setImporting(false) + if (importInputRef.current) importInputRef.current.value = '' + } + } + const applyFilters = () => { setPreset(null) loadTrips() @@ -87,6 +150,32 @@ export default function Trips() { className="text-sm outline-none bg-transparent" />
+ + Template + + + handleImportTripsCsv(e.target.files?.[0])} + /> +
+ {/* Import feedback (mobile + status) */} + {(importError || importSummary) && ( +
+ {importError ? ( + Import failed: {importError} + ) : ( + + Imported: {importSummary.created} created, {importSummary.errors} errors (from {importSummary.total_rows} rows) + + )} +
+ + Template + + +
+
+ )} + {/* Quick filter presets */} { setPreset(p); clearFilters() }} /> @@ -216,6 +335,124 @@ export default function Trips() { ))}
)} + + {/* Add Trip Modal */} + {showAddTrip && ( +
+
+
+
+

Add trip

+

Manual entry (individual trip)

+
+ +
+ +
+ {createError && ( +
+ Error: {createError} +
+ )} + +
+
+ + setNewTrip(t => ({ ...t, date: e.target.value }))} + className="w-full border border-uber-gray-200 rounded-lg px-3 py-2 text-sm outline-none" + required + /> +
+
+ + setNewTrip(t => ({ ...t, start_time: e.target.value }))} + className="w-full border border-uber-gray-200 rounded-lg px-3 py-2 text-sm outline-none" + required + /> +
+
+ + setNewTrip(t => ({ ...t, end_time: e.target.value }))} + className="w-full border border-uber-gray-200 rounded-lg px-3 py-2 text-sm outline-none" + required + /> +
+
+ + setNewTrip(t => ({ ...t, distance_km: e.target.value }))} + className="w-full border border-uber-gray-200 rounded-lg px-3 py-2 text-sm outline-none" + placeholder="8.2" + required + /> +
+
+ + setNewTrip(t => ({ ...t, fare: e.target.value }))} + className="w-full border border-uber-gray-200 rounded-lg px-3 py-2 text-sm outline-none" + placeholder="310" + required + /> +
+
+ + setNewTrip(t => ({ ...t, stress_score: e.target.value }))} + className="w-full border border-uber-gray-200 rounded-lg px-3 py-2 text-sm outline-none" + placeholder="0.0" + /> +
+
+ +
+ + +
+
+
+
+ )}
) } From 46b42474213682c23bcc248cb76701ed29b361bc Mon Sep 17 00:00:00 2001 From: RishitGG Date: Mon, 9 Mar 2026 21:15:25 +0530 Subject: [PATCH 07/13] unit tests and bug fixes --- README.md | 38 ++++++++- backend/data/batch_processor.py | 63 +++++++++----- backend/data/config.py | 9 ++ backend/data/sample_data.py | 82 +++++++------------ backend/main.py | 3 + backend/utils/logging.py | 20 +++++ frontend/package-lock.json | 11 ++- .../src/__tests__/EarningsProgress.test.jsx | 32 ++++++++ frontend/src/__tests__/TripsAddTrip.test.jsx | 43 ++++++++++ frontend/src/api/client.js | 8 +- frontend/src/components/EarningsProgress.jsx | 44 +++++++--- frontend/src/pages/Dashboard.jsx | 9 ++ frontend/src/pages/Goals.jsx | 18 +++- frontend/src/pages/Trips.jsx | 35 ++++++++ frontend/src/utils/sanityChecks.js | 19 +++++ tests/data/earnings_batch_example.csv | 5 ++ tests/data/stress_batch_example.csv | 4 + tests/data/trips_import_example.csv | 4 + 18 files changed, 357 insertions(+), 90 deletions(-) create mode 100644 backend/data/config.py create mode 100644 backend/utils/logging.py create mode 100644 frontend/src/__tests__/EarningsProgress.test.jsx create mode 100644 frontend/src/__tests__/TripsAddTrip.test.jsx create mode 100644 frontend/src/utils/sanityChecks.js create mode 100644 tests/data/earnings_batch_example.csv create mode 100644 tests/data/stress_batch_example.csv create mode 100644 tests/data/trips_import_example.csv diff --git a/README.md b/README.md index bd3c6fe..8730432 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Real-time driver wellness & earnings intelligence platform for ride-hailing driv - **Explainability** — Per-event feature contributions, confidence badges - **Feedback** — Thumbs up/down on detected events +To log **multiple trips at once**, go to the `Trips` tab and use **Import CSV**. + --- ## Architecture @@ -23,7 +25,7 @@ Real-time driver wellness & earnings intelligence platform for ride-hailing driv Driver-Pulse/ ├── backend/ # FastAPI server │ ├── main.py # API endpoints -│ └── data/ # Sample data + batch inference +│ └── data/ # Sample data, goals, trips import, batch inference ├── frontend/ # React + Vite + Tailwind │ └── src/ │ ├── pages/ # Dashboard, Trips, TripDetail, Trends, Goals, Predict, BatchUpload @@ -33,6 +35,18 @@ Driver-Pulse/ └── requirements.txt ``` +At a high level, the React frontend talks to a single FastAPI backend (`/api/*`), which serves demo data from an in-memory store and calls local ML helpers for stress and earnings predictions. + +```mermaid +flowchart LR + browser[Browser_ReactApp] --> api[FastAPI_Backend] + api --> tripsStore[InMemory_Trips_+_Goals] + api --> stressBatch[Stress_Batch_Processor] + api --> earningsBatch[Earnings_Batch_Processor] + stressBatch --> stressModel[Stress_Model_Files] + earningsBatch --> earningsModel[Earnings_Model_Files] +``` + --- ## Setup @@ -65,3 +79,25 @@ Open **http://localhost:5173** in your browser. | Frontend | React 18, Vite, Tailwind CSS, Recharts, Leaflet | | Backend | FastAPI, Uvicorn | | ML | scikit-learn, NumPy, Pandas | + +--- + +## Data Flow + +- **Trips & goals**: Manual entry or CSV import hit `/api/trips` or `/api/trips/import-csv`, which update an in-memory trips list. Goals (`/api/goals`) and dashboard (`/api/dashboard`) recompute current earnings, hours, and forecast from those trips. +- **Batch stress & earnings**: Batch CSV uploads are processed by backend helpers that engineer features, call local models, and return per-row predictions plus summaries as JSON. + +--- + +## Scalability & Modularity + +- **Backend**: FastAPI routes in `backend/main.py` delegate to small modules in `backend/data/` for trips, goals, imports, and batch processing, so swapping the in-memory store for a database or separate ML service is a local change. +- **Frontend**: The React app uses a single API client layer (`frontend/src/api/client.js`) plus page/component separation, making it easy to plug in global state, auth, or feature flags without rewriting screens. +- **Batch endpoints**: Batch CSV processing is stateless per request, so multiple backend instances can handle uploads in parallel behind a load balancer. + +--- + +## Testing & Validation Notes + +- **Frontend sanity checks** — lightweight helpers in `frontend/src/utils/sanityChecks.js` validate money inputs, time ranges, and clamp goal targets. +- **Example test files** — illustrative, non-wired tests live in `frontend/src/__tests__/` (e.g. `EarningsProgress.test.jsx`, `TripsAddTrip.test.jsx`) to show how key components and behaviours could be validated in a full test setup. diff --git a/backend/data/batch_processor.py b/backend/data/batch_processor.py index ab43d41..3962f78 100644 --- a/backend/data/batch_processor.py +++ b/backend/data/batch_processor.py @@ -13,6 +13,8 @@ from pathlib import Path from typing import Optional +from .config import BATCH_ROW_SOFT_LIMIT + ROOT = Path(__file__).resolve().parent.parent.parent # project root (Driver-Pulse/) # ── Stress model ────────────────────────────────────────────── @@ -277,6 +279,12 @@ def process_stress_csv(csv_content: str) -> dict: if not rows: return {"error": "CSV is empty", "results": [], "summary": {}} + row_count = len(rows) + row_limit_warning = None + if BATCH_ROW_SOFT_LIMIT and row_count > BATCH_ROW_SOFT_LIMIT: + row_limit_warning = ( + f"Processed {row_count} rows; consider chunking to {BATCH_ROW_SOFT_LIMIT} rows per upload for large workloads." + ) results = [] severity_counts = {"low": 0, "medium": 0, "high": 0} situation_counts = {} @@ -314,21 +322,25 @@ def process_stress_csv(csv_content: str) -> dict: notify_count = sum(1 for r in results if r["should_notify"]) safety_count = sum(1 for r in results if r["is_safety_critical"]) + summary = { + "total_windows": total, + "severity_counts": severity_counts, + "situation_counts": situation_counts, + "avg_confidence": avg_confidence, + "notifications_triggered": notify_count, + "safety_critical_count": safety_count, + "stress_score": round( + (severity_counts.get("high", 0) * 5 + severity_counts.get("medium", 0) * 3) + / max(total, 1), + 2, + ), + } + if row_limit_warning: + summary["row_limit_warning"] = row_limit_warning + return { "results": results, - "summary": { - "total_windows": total, - "severity_counts": severity_counts, - "situation_counts": situation_counts, - "avg_confidence": avg_confidence, - "notifications_triggered": notify_count, - "safety_critical_count": safety_count, - "stress_score": round( - (severity_counts.get("high", 0) * 5 + severity_counts.get("medium", 0) * 3) - / max(total, 1), - 2, - ), - }, + "summary": summary, } @@ -341,6 +353,13 @@ def process_earnings_csv(csv_content: str) -> dict: if df.empty: return {"error": "CSV is empty", "results": [], "summary": {}} + row_count = len(df) + row_limit_warning = None + if BATCH_ROW_SOFT_LIMIT and row_count > BATCH_ROW_SOFT_LIMIT: + row_limit_warning = ( + f"Processed {row_count} rows; consider chunking to {BATCH_ROW_SOFT_LIMIT} rows per upload for large workloads." + ) + # Fill defaults for missing columns for col, default in [ ("driver_id", "driver-001"), @@ -383,15 +402,19 @@ def process_earnings_csv(csv_content: str) -> dict: for r in results: forecast_counts[r.get("forecast_status", "on_track")] += 1 + summary = { + "total_entries": total, + "avg_predicted_velocity": avg_velocity, + "forecast_counts": forecast_counts, + "best_velocity": max((r["predicted_velocity"] for r in results if r["predicted_velocity"]), default=0), + "worst_velocity": min((r["predicted_velocity"] for r in results if r["predicted_velocity"]), default=0), + } + if row_limit_warning: + summary["row_limit_warning"] = row_limit_warning + return { "results": results, - "summary": { - "total_entries": total, - "avg_predicted_velocity": avg_velocity, - "forecast_counts": forecast_counts, - "best_velocity": max((r["predicted_velocity"] for r in results if r["predicted_velocity"]), default=0), - "worst_velocity": min((r["predicted_velocity"] for r in results if r["predicted_velocity"]), default=0), - }, + "summary": summary, } diff --git a/backend/data/config.py b/backend/data/config.py new file mode 100644 index 0000000..7911bef --- /dev/null +++ b/backend/data/config.py @@ -0,0 +1,9 @@ +""" +Shared configuration and limits for backend demo modules. +Kept small and simple on purpose so it is easy to swap out in a real deployment. +""" + +# Soft limit for batch CSV rows. Requests above this size are still processed, +# but responses can include a warning so callers know to chunk work if needed. +BATCH_ROW_SOFT_LIMIT = 5000 + diff --git a/backend/data/sample_data.py b/backend/data/sample_data.py index c01fade..e6e68da 100644 --- a/backend/data/sample_data.py +++ b/backend/data/sample_data.py @@ -477,15 +477,39 @@ def get_goals(): goals["trips_completed"] = trips_completed goals["current_velocity"] = current_velocity - # Keep dependent values fresh (even if target was set earlier). - remaining_earnings = max(0, float(goals.get("daily_target") or 0) - current_earnings) - remaining_hours = max(0.1, float(goals.get("target_hours") or 0) - current_hours) + _recompute_goal_derivatives(goals) + _GOALS = goals + return goals + + +def set_goal_target(target: float): + global _GOALS + goals = get_goals() + goals["daily_target"] = float(target) + + _recompute_goal_derivatives(goals) + + _GOALS = goals + return _GOALS + + +def _recompute_goal_derivatives(goals: Dict[str, Any]) -> None: + """ + Keep all goal-derived fields (remaining earnings/hours, required velocity, + probability and forecast status) in one place so they stay consistent + between get_goals() and set_goal_target(). + """ + remaining_earnings = max(0, float(goals.get("daily_target") or 0) - float(goals.get("current_earnings") or 0)) + remaining_hours = max(0.1, float(goals.get("target_hours") or 0) - float(goals.get("current_hours") or 0)) goals["required_velocity"] = round(remaining_earnings / remaining_hours, 0) if goals.get("daily_target") else 0 goals["remaining_earnings"] = round(remaining_earnings, 2) - goals["remaining_hours"] = round(max(0.0, float(goals.get("target_hours") or 0) - current_hours), 2) + goals["remaining_hours"] = round( + max(0.0, float(goals.get("target_hours") or 0) - float(goals.get("current_hours") or 0)), + 2, + ) + current_velocity = float(goals.get("current_velocity") or 0) goals["hours_to_target"] = round(remaining_earnings / current_velocity, 2) if current_velocity > 0 else None - # Recompute forecast probability + status based on current pace vs required pace. velocity_ratio = goals["current_velocity"] / max(goals["required_velocity"], 1) time_ratio = goals["current_hours"] / max(goals["target_hours"], 0.1) @@ -498,7 +522,6 @@ def get_goals(): else: prob = 0.25 - # Adjust based on time progress (less time left, harder to catch up). if time_ratio > 0.8: prob *= 0.8 @@ -510,50 +533,3 @@ def get_goals(): goals["forecast_status"] = "on_track" else: goals["forecast_status"] = "at_risk" - - _GOALS = goals - return goals - - -def set_goal_target(target: float): - global _GOALS - goals = get_goals() - goals["daily_target"] = float(target) - - # Recalculate dependent values - remaining_earnings = max(0, goals["daily_target"] - goals["current_earnings"]) - remaining_hours = max(0.1, goals["target_hours"] - goals["current_hours"]) - goals["required_velocity"] = round(remaining_earnings / remaining_hours, 0) - goals["remaining_earnings"] = round(remaining_earnings, 2) - goals["remaining_hours"] = round(max(0.0, goals["target_hours"] - goals["current_hours"]), 2) - goals["hours_to_target"] = round(remaining_earnings / goals["current_velocity"], 2) if goals["current_velocity"] > 0 else None - - # Simple probability calculation based on current velocity vs required - velocity_ratio = goals["current_velocity"] / max(goals["required_velocity"], 1) - time_ratio = goals["current_hours"] / goals["target_hours"] - - if velocity_ratio >= 1.2: - prob = 0.95 - elif velocity_ratio >= 1.0: - prob = 0.75 - elif velocity_ratio >= 0.8: - prob = 0.55 - else: - prob = 0.25 - - # Adjust based on time progress - if time_ratio > 0.8: - prob *= 0.8 # Less time left, harder to catch up - - goals["goal_probability"] = round(min(0.99, max(0.01, prob)), 2) - - # Update forecast status - if goals["current_velocity"] >= goals["required_velocity"] * 1.1: - goals["forecast_status"] = "ahead" - elif goals["current_velocity"] >= goals["required_velocity"] * 0.9: - goals["forecast_status"] = "on_track" - else: - goals["forecast_status"] = "at_risk" - - _GOALS = goals - return _GOALS diff --git a/backend/main.py b/backend/main.py index cea8597..d264abe 100644 --- a/backend/main.py +++ b/backend/main.py @@ -10,6 +10,8 @@ from typing import Optional, List from datetime import datetime +from utils.logging import log_info, log_warn + from data.sample_data import ( get_trips, get_profile, get_goals, set_goal_target, build_dashboard, build_weekly_metrics, build_monthly_metrics, @@ -83,6 +85,7 @@ class TripCreatePayload(BaseModel): @app.get("/api/health") def health(): + log_info("health check") return {"status": "ok", "timestamp": datetime.now().isoformat()} diff --git a/backend/utils/logging.py b/backend/utils/logging.py new file mode 100644 index 0000000..661fdb5 --- /dev/null +++ b/backend/utils/logging.py @@ -0,0 +1,20 @@ +""" +Very small logging helper for the demo backend. +In a real deployment this is where structured logging or APM hooks would live. +""" + +from datetime import datetime + + +def _ts() -> str: + return datetime.now().isoformat() + + +def log_info(message: str) -> None: + print(f"[info] { _ts() } {message}") + + +def log_warn(message: str) -> None: + print(f"[warn] { _ts() } {message}") + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 302a2bd..2ad91dd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -70,6 +70,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1463,6 +1464,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2046,6 +2048,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -2086,7 +2089,8 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/lilconfig": { "version": "3.1.3", @@ -2310,6 +2314,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2496,6 +2501,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -2508,6 +2514,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -2938,6 +2945,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3031,6 +3039,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/frontend/src/__tests__/EarningsProgress.test.jsx b/frontend/src/__tests__/EarningsProgress.test.jsx new file mode 100644 index 0000000..cce50bd --- /dev/null +++ b/frontend/src/__tests__/EarningsProgress.test.jsx @@ -0,0 +1,32 @@ +// NOTE: Illustrative test only – runner not wired. +// Shows how EarningsProgress could be tested with a real test setup. + +import EarningsProgress from '../components/EarningsProgress' + +const mockGoals = { + daily_target: 1800, + current_earnings: 900, + current_velocity: 200, + required_velocity: 225, + goal_probability: 0.75, + current_hours: 4.5, + target_hours: 10, + trips_completed: 6, + forecast_status: 'on_track', +} + +// Pseudo-test demonstrating expected derived values. +function exampleUsage() { + const pct = Math.round((mockGoals.current_earnings / mockGoals.daily_target) * 100) + const remaining = mockGoals.daily_target - mockGoals.current_earnings + + // In a real test you might assert: + // expect(screen.getByText(`₹${mockGoals.current_earnings.toLocaleString()}`)).toBeInTheDocument() + // expect(screen.getByText(`${pct}% achieved`)).toBeInTheDocument() + // expect(screen.getByText(`₹${remaining.toLocaleString()} remaining`)).toBeInTheDocument() + + return { pct, remaining } +} + +export { EarningsProgress, mockGoals, exampleUsage } + diff --git a/frontend/src/__tests__/TripsAddTrip.test.jsx b/frontend/src/__tests__/TripsAddTrip.test.jsx new file mode 100644 index 0000000..648a327 --- /dev/null +++ b/frontend/src/__tests__/TripsAddTrip.test.jsx @@ -0,0 +1,43 @@ +// NOTE: Illustrative test only – runner not wired. +// Shows intended behaviour for adding trips vs goals aggregation. + +import { isValidTimeRange } from '../utils/sanityChecks' + +// Minimal shapes copied from backend logic for explanation purposes only. +const TODAY = '2026-03-08' + +function goalsFromTrips(trips, goals) { + const todayTrips = trips.filter(t => t.date === TODAY) + const current_earnings = todayTrips.reduce((sum, t) => sum + (t.fare || 0), 0) + const current_hours = todayTrips.reduce((sum, t) => sum + (t.duration_min || 0), 0) / 60 + + return { + ...goals, + current_earnings, + current_hours, + } +} + +// Pseudo-test: adding a trip on a non-today date should NOT affect today's goals. +function exampleTripBehaviour() { + const baseGoals = { daily_target: 1800, current_earnings: 1000, current_hours: 5 } + + const trips = [ + { id: 't1', date: TODAY, fare: 500, duration_min: 30 }, + { id: 't2', date: TODAY, fare: 500, duration_min: 30 }, + ] + + const withTodayGoals = goalsFromTrips(trips, baseGoals) + + const newTripOtherDay = { id: 't3', date: '2026-03-07', fare: 400, duration_min: 40 } + const withOtherDayTripGoals = goalsFromTrips([...trips, newTripOtherDay], baseGoals) + + // In a real test you might assert: + // expect(withOtherDayTripGoals.current_earnings).toBe(withTodayGoals.current_earnings) + // expect(withOtherDayTripGoals.current_hours).toBe(withTodayGoals.current_hours) + + return { withTodayGoals, withOtherDayTripGoals } +} + +export { isValidTimeRange, goalsFromTrips, exampleTripBehaviour } + diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index ea0e927..11078aa 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -1,11 +1,17 @@ const BASE = '/api'; +// Single place to plug in auth headers, retries, error normalisation, etc. async function request(path, options = {}) { const res = await fetch(`${BASE}${path}`, { headers: { 'Content-Type': 'application/json', ...options.headers }, ...options, }); - if (!res.ok) throw new Error(`API ${res.status}: ${res.statusText}`); + if (!res.ok) { + const message = `API ${res.status}: ${res.statusText}`; + const error = new Error(message); + error.status = res.status; + throw error; + } return res.json(); } diff --git a/frontend/src/components/EarningsProgress.jsx b/frontend/src/components/EarningsProgress.jsx index faa8e5a..2b05641 100644 --- a/frontend/src/components/EarningsProgress.jsx +++ b/frontend/src/components/EarningsProgress.jsx @@ -1,6 +1,12 @@ export default function EarningsProgress({ goals }) { if (!goals) return null - const pct = Math.min(100, Math.round((goals.current_earnings / goals.daily_target) * 100)) + + const currentEarnings = Number(goals.current_earnings || 0) + const dailyTarget = Number(goals.daily_target || 0) + const safeTarget = dailyTarget > 0 ? dailyTarget : 0 + const pct = safeTarget > 0 + ? Math.min(100, Math.round((currentEarnings / safeTarget) * 100)) + : 0 const statusColor = { ahead: 'text-uber-green', @@ -8,6 +14,11 @@ export default function EarningsProgress({ goals }) { at_risk: 'text-uber-red', }[goals.forecast_status] || 'text-uber-gray-500' + const prettyStatus = + typeof goals.forecast_status === 'string' + ? goals.forecast_status.replace('_', ' ') + : 'on track' + return (
@@ -18,8 +29,8 @@ export default function EarningsProgress({ goals }) {
- ₹{goals.current_earnings.toLocaleString()} - / ₹{goals.daily_target.toLocaleString()} + ₹{currentEarnings.toLocaleString()} + / ₹{safeTarget.toLocaleString()}
{/* Progress bar */} @@ -31,7 +42,7 @@ export default function EarningsProgress({ goals }) {
{pct}% achieved - ₹{(goals.daily_target - goals.current_earnings).toLocaleString()} remaining + ₹{Math.max(0, safeTarget - currentEarnings).toLocaleString()} remaining
{/* Extra stats */} @@ -45,7 +56,12 @@ export default function EarningsProgress({ goals }) {

Required ₹/hr

-

{Math.round(goals.goal_probability * 100)}%

+

+ {Number.isFinite(goals.goal_probability) + ? Math.round(goals.goal_probability * 100) + : 0} + % +

Probability

@@ -54,7 +70,7 @@ export default function EarningsProgress({ goals }) {

Remaining

-

₹{Math.max(0, goals.daily_target - goals.current_earnings).toLocaleString()}

+

₹{Math.max(0, safeTarget - currentEarnings).toLocaleString()}

to reach target

@@ -69,12 +85,16 @@ export default function EarningsProgress({ goals }) {

Status

-

- {goals.forecast_status?.replace('_', ' ').charAt(0).toUpperCase() + goals.forecast_status?.replace('_', ' ').slice(1)} +

+ {prettyStatus.charAt(0).toUpperCase() + prettyStatus.slice(1)}

today's pace

diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index eb72a53..2e10106 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -13,6 +13,7 @@ export default function Dashboard() { const [goals, setGoals] = useState(null) const [selectedDay, setSelectedDay] = useState('today') const [loading, setLoading] = useState(true) + const [error, setError] = useState('') const today = '2026-03-08' const yesterday = '2026-03-07' @@ -24,6 +25,7 @@ export default function Dashboard() { const loadData = async () => { setLoading(true) try { + setError('') const [dash, tripRes, goalsRes] = await Promise.all([ api.getDashboard(), api.getTrips({ date: selectedDay === 'today' ? today : yesterday }), @@ -34,6 +36,7 @@ export default function Dashboard() { setGoals(goalsRes) } catch (err) { console.error('Failed to load dashboard:', err) + setError('Unable to load latest dashboard data. Please try again in a moment.') } finally { setLoading(false) } @@ -90,6 +93,12 @@ export default function Dashboard() {
)} + {error && ( +

+ {error} +

+ )} + {/* Timeline + Earnings */}
diff --git a/frontend/src/pages/Goals.jsx b/frontend/src/pages/Goals.jsx index b22b096..2ff6226 100644 --- a/frontend/src/pages/Goals.jsx +++ b/frontend/src/pages/Goals.jsx @@ -3,6 +3,7 @@ import { api } from '../api/client' import EarningsProgress from '../components/EarningsProgress' import StressTips from '../components/StressTips' import { Target, Save, CheckCircle, TrendingUp, Clock, DollarSign } from 'lucide-react' +import { clampDailyTarget, isValidMoney } from '../utils/sanityChecks' export default function Goals() { const [goals, setGoals] = useState(null) @@ -10,6 +11,7 @@ export default function Goals() { const [targetInput, setTargetInput] = useState('') const [saving, setSaving] = useState(false) const [saved, setSaved] = useState(false) + const [error, setError] = useState('') useEffect(() => { api.getGoals() @@ -22,14 +24,20 @@ export default function Goals() { }, []) const handleSave = async () => { + setError('') + const clamped = clampDailyTarget(targetInput) + if (!isValidMoney(clamped) || clamped <= 0) { + setError('Please enter a positive daily target') + return + } setSaving(true) try { - const updated = await api.setGoal(Number(targetInput)) + const updated = await api.setGoal(Number(clamped)) setGoals(updated) setSaved(true) setTimeout(() => setSaved(false), 2000) } catch { - alert('Failed to save goal') + setError('Failed to save goal') } finally { setSaving(false) } @@ -82,6 +90,12 @@ export default function Goals() {
+ {error && ( +

+ {error} +

+ )} +

Set your daily earnings target. Your progress will be tracked in real-time on the dashboard.

diff --git a/frontend/src/pages/Trips.jsx b/frontend/src/pages/Trips.jsx index 3424f47..39153a9 100644 --- a/frontend/src/pages/Trips.jsx +++ b/frontend/src/pages/Trips.jsx @@ -3,6 +3,7 @@ import { api } from '../api/client' import TripListItem from '../components/TripListItem' import FilterChips from '../components/FilterChips' import { Calendar, SlidersHorizontal, Plus, Upload, Download, X } from 'lucide-react' +import { isValidMoney, isValidTimeRange } from '../utils/sanityChecks' export default function Trips() { const [trips, setTrips] = useState([]) @@ -68,6 +69,30 @@ export default function Trips() { const submitNewTrip = async (e) => { e.preventDefault() setCreateError('') + // Basic client-side validation mirroring backend rules + if (!newTrip.date) { + setCreateError('Date is required') + return + } + if (!isValidTimeRange(newTrip.start_time, newTrip.end_time)) { + setCreateError('End time must be after start time') + return + } + if (!isValidMoney(newTrip.distance_km)) { + setCreateError('Please enter a valid distance') + return + } + if (!isValidMoney(newTrip.fare)) { + setCreateError('Please enter a valid fare') + return + } + if (newTrip.stress_score !== '' && newTrip.stress_score !== null && newTrip.stress_score !== undefined) { + const s = Number(newTrip.stress_score) + if (!Number.isFinite(s) || s < 0 || s > 10) { + setCreateError('Stress score must be a number between 0 and 10') + return + } + } setCreating(true) try { const payload = { @@ -93,6 +118,16 @@ export default function Trips() { const handleImportTripsCsv = async (file) => { if (!file) return + if (!file.name.toLowerCase().endsWith('.csv')) { + setImportError('Please upload a .csv file') + return + } + // 1 MB soft limit – avoids accidental huge files + const maxSizeBytes = 1 * 1024 * 1024 + if (file.size > maxSizeBytes) { + setImportError('CSV is too large (max 1MB)') + return + } setImportError('') setImportSummary(null) setImporting(true) diff --git a/frontend/src/utils/sanityChecks.js b/frontend/src/utils/sanityChecks.js new file mode 100644 index 0000000..efb9bcc --- /dev/null +++ b/frontend/src/utils/sanityChecks.js @@ -0,0 +1,19 @@ +export function isValidMoney(value) { + const n = Number(value); + return Number.isFinite(n) && n >= 0; +} + +export function isValidTimeRange(start, end) { + if (!start || !end) return false; + // HH:MM 24h format compares correctly as strings + return end > start; +} + +export function clampDailyTarget(target, min = 0, max = 10000) { + const n = Number(target); + if (!Number.isFinite(n)) return min; + if (n < min) return min; + if (n > max) return max; + return n; +} + diff --git a/tests/data/earnings_batch_example.csv b/tests/data/earnings_batch_example.csv new file mode 100644 index 0000000..44fbaa3 --- /dev/null +++ b/tests/data/earnings_batch_example.csv @@ -0,0 +1,5 @@ +driver_id,timestamp,cumulative_earnings,elapsed_hours,current_velocity,target_velocity,velocity_delta,trips_completed,target_earnings +driver-001,08:00:00,0,0.5,0,200,0,0,1800 +driver-001,09:00:00,185,1.5,185,200,-15,2,1800 +driver-001,10:00:00,420,2.5,210,200,10,4,1800 + diff --git a/tests/data/stress_batch_example.csv b/tests/data/stress_batch_example.csv new file mode 100644 index 0000000..1afe748 --- /dev/null +++ b/tests/data/stress_batch_example.csv @@ -0,0 +1,4 @@ +trip_id,timestamp,motion_max,motion_mean,motion_p95,brake_intensity,lateral_max,speed_mean,speed_at_brake,speed_drop,audio_db_max,audio_db_mean,audio_db_p90,audio_db_std,cadence_var_mean,argument_frac,loud_frac +trip-001,08:15:00,1.2,0.6,1.1,0.8,0.5,35,35,5,68,62,67,4,0.1,0.15 +trip-002,09:30:00,5.1,1.8,4.8,4.9,2.1,40,40,25,94,82,91,8,0.72,0.95 + diff --git a/tests/data/trips_import_example.csv b/tests/data/trips_import_example.csv new file mode 100644 index 0000000..50af253 --- /dev/null +++ b/tests/data/trips_import_example.csv @@ -0,0 +1,4 @@ +date,start_time,end_time,distance_km,fare,stress_score +2026-03-08,09:15,09:45,8.2,310,2.5 +2026-03-08,18:05,18:40,11.6,520,6.3 + From ad151ebb1b0182876440a580be61b8dabb90de7c Mon Sep 17 00:00:00 2001 From: Merin Theres Jose Date: Mon, 9 Mar 2026 22:40:40 +0530 Subject: [PATCH 08/13] readme.md edits --- README.md | 58 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8730432..a106ec5 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,11 @@ Real-time driver wellness & earnings intelligence platform for ride-hailing driv - **Trip Detail** — Map playback, sensor charts, event detection with explainability - **Trends** — Weekly/monthly earnings, stress, and velocity charts - **Goals** — Set and track daily earnings targets -- **Manual Predict** — Enter sensor/earnings values → instant ML prediction -- **Batch Upload** — Upload CSV → run inference on multiple trips at once +- **Predict** — Enter sensor/earnings values → instant ML prediction *(judge-facing)* +- **Batch Upload** — Upload CSV → run inference on multiple trips at once *(judge-facing)* - **Explainability** — Per-event feature contributions, confidence badges - **Feedback** — Thumbs up/down on detected events +- **Auth** — Login / register with demo accounts or new profile To log **multiple trips at once**, go to the `Trips` tab and use **Import CSV**. @@ -23,20 +24,51 @@ To log **multiple trips at once**, go to the `Trips` tab and use **Import CSV**. ``` Driver-Pulse/ -├── backend/ # FastAPI server -│ ├── main.py # API endpoints -│ └── data/ # Sample data, goals, trips import, batch inference -├── frontend/ # React + Vite + Tailwind +├── backend/ # FastAPI REST API (25 endpoints) +│ ├── main.py # Routes, middleware, Pydantic models +│ ├── data/ +│ │ ├── sample_data.py # Synthetic trip/route/event generator +│ │ ├── batch_processor.py # Loads ML models, runs batch inference +│ │ ├── trips_import.py # CSV trip import parser +│ │ ├── users.py # In-memory auth store +│ │ └── config.py # Batch limits & constants +│ └── utils/ +│ └── logging.py # Timestamped structured logging +│ +├── frontend/ # React 18 + Vite + Tailwind SPA │ └── src/ -│ ├── pages/ # Dashboard, Trips, TripDetail, Trends, Goals, Predict, BatchUpload -│ └── components/ # Sidebar, TripMap, SignalCharts, ExplainModal, etc. -├── drivepulse_stress_model/ # Stress ML pipeline -├── earnings/ # Earnings ML pipeline -└── requirements.txt +│ ├── pages/ # 8 pages: Home, Dashboard, Trips, TripDetail, +│ │ # Trends, Goals, Predict, BatchUpload +│ ├── components/ # 16 reusable components +│ ├── api/client.js # Centralised API client +│ └── utils/sanityChecks.js # Input validation helpers +│ +├── drivepulse_stress_model/ # Stress Detection ML pipeline +│ ├── run.py # CLI entry (--generate --calibrate --train --demo) +│ ├── src/ +│ │ ├── generate_data.py # Synthetic sensor window generator (3,150 samples) +│ │ ├── train.py # RF classifier training + evaluation +│ │ ├── inference.py # InferenceEngine with rule-based fallback +│ │ └── hal.py # Hardware Abstraction Layer (device calibration) +│ ├── model/ # Trained artifacts (rf_model.pkl, baselines, contract) +│ └── calibration/ # Device calibration profile +│ +├── earnings/earnings/ # Earnings Forecasting ML pipeline +│ ├── run.py # Sequential pipeline entry +│ ├── src/ +│ │ ├── build_dataset.py # Merges drivers + goals + velocity + trips +│ │ ├── features.py # 14-feature engineering (lags, rolling avg, rush flags) +│ │ ├── augment.py # 5× Gaussian noise augmentation +│ │ ├── train.py # RF regressor training + evaluation +│ │ └── inference.py # Batch velocity prediction +│ ├── model/ # Trained artifacts (rf_model.pkl, contract) +│ └── data/ # Source CSVs (drivers, goals, velocity, trips) +│ +├── streamlit_app.py # Standalone Streamlit demo (3 tabs) +├── tests/data/ # Example CSVs for batch & import testing +└── requirements.txt # Root Python dependencies ``` -At a high level, the React frontend talks to a single FastAPI backend (`/api/*`), which serves demo data from an in-memory store and calls local ML helpers for stress and earnings predictions. - ```mermaid flowchart LR browser[Browser_ReactApp] --> api[FastAPI_Backend] From 6e429dc576b0b68b4774e0feda165500d5fb324e Mon Sep 17 00:00:00 2001 From: Merin Theres Jose Date: Tue, 10 Mar 2026 00:23:11 +0530 Subject: [PATCH 09/13] docs: add system architecture, design documentation, and development progress log --- docs/PROGRESS_LOG.md | 27 +++ docs/architecture_explanation.html | 286 +++++++++++++++++++++++++++++ docs/architecture_image.png | Bin 0 -> 244147 bytes docs/design.html | 282 ++++++++++++++++++++++++++++ 4 files changed, 595 insertions(+) create mode 100644 docs/PROGRESS_LOG.md create mode 100644 docs/architecture_explanation.html create mode 100644 docs/architecture_image.png create mode 100644 docs/design.html diff --git a/docs/PROGRESS_LOG.md b/docs/PROGRESS_LOG.md new file mode 100644 index 0000000..8e472ec --- /dev/null +++ b/docs/PROGRESS_LOG.md @@ -0,0 +1,27 @@ +# Progress Log (Development History) + +A chronological log of our problem-solving process and major iterations. + + +--- + +**Mar 4:** Project kickoff. Team discussion on requirements, user persona, and value proposition. Decided on stress + earnings as dual focus. + +**Mar 5:** Finalized tech stack (FastAPI, React, scikit-learn). Planned architecture and repo structure. Set up the project repository and initial folder structure. + +**Mar 6:** Backend development: implemented API skeleton (trips, goals, dashboard), created synthetic trip data generator, and batch processor. + +**Mar 7:** Backend ML: built and trained initial stress detection (RF, HAL, Platt calibration) and earnings velocity models. Added endpoints for batch inference and CSV import. + +**Mar 8:** Frontend development: built dashboard, trip list, trip detail, earnings progress bar. Integrated backend APIs. Added batch upload and explainability features. + +**Mar 9:** Documentation: wrote design doc (HTML), architecture diagram (SVG), and progress log. Re-checked all features, tested flows, and made final edits based on feedback. Prepared for deployment. + +**Mar 10:** Deployment and submission: deployed backend and frontend, final bugfixes, submitted project. + +--- + +**Outcome:** +Successfully built and deployed DrivePulse — a driver safety and earnings intelligence platform that transforms raw trip signals into actionable insights. The system combines real-time stress detection with earnings velocity forecasting while maintaining privacy through on-device processing. All core features (stress detection, earnings forecasting, explainability tools, batch testing, and feedback collection) were implemented as planned. Documentation, architecture diagrams, and progress logs were completed and the project was submitted on time. + +--- \ No newline at end of file diff --git a/docs/architecture_explanation.html b/docs/architecture_explanation.html new file mode 100644 index 0000000..882c5fa --- /dev/null +++ b/docs/architecture_explanation.html @@ -0,0 +1,286 @@ + + + + + + DrivePulse — System Architecture + + + + + +
+

🚗 DrivePulse

+

System Architecture

+
UBER SHE++ HACKATHON 2026
+
MerinRishitLavisha
+
+ + + +
+ + +
+

01 Architecture Diagram

+

End-to-end data flow: sensors → on-device processing → server → driver insights → feedback loop.

+ +
+ + + + + + + ON-DEVICE (EDGE) + + + OFF-DEVICE (SERVER) + + + FEEDBACK LOOP + + + + 📱 Accelerometer + 10 Hz x/y/z motion + + + 🎤 Microphone + dB envelope only + + + 📍 GPS + Speed + km/h, lat/lng + + + 💰 Trip Earnings + ₹ per trip, hours + + + + + + + + + HAL Calibration + Device normalisation + AGC + road baseline + + + + + + + Feature Extraction + 30s windows, Z-score norm + ~12 stress / 14 earnings feats + + + + + + + + + + Local ML Inference + RF Classifier (stress) · <1ms + RF Regressor (earnings) + + + + REST + + + + + + + 🔔 Driver Alerts + Real-time, on-device + + + + 📊 Driver Dashboard + Glanceable UI + + + + + + + FastAPI Backend + 25 REST endpoints + Auth, CORS, validation + + + + Data Store + In-memory (MVP) + Trips, goals, events + + + + + Batch Processor + CSV upload → inference + Stress + Earnings models + + + + + Model Artifacts + rf_model.pkl (×2) + baselines, contracts + + + + + Aggregation Engine + Dashboard summaries + Trends, goal tracking + + + + + Driver Web App (React) + 8 pages, 16 components + Maps, charts, modals + + + + + + + 👍👎 Feedback + Collected per event + + + + + 🔄 Retrain Loop + Future: feedback → model + + + + + + + + + + + + DrivePulse — End-to-End Data Flow + + +

Right-click → "Save image as" or screenshot this diagram to export as PNG.

+
+
+ + +
+

02 Architecture Explanation

+

Design decisions and engineering trade-offs.

+ +

Real-Time vs. Post-Trip Processing

+
    +
  • Stress detection → real-time, on-device. RF classifier runs per 30s window in <1ms. Driver gets alerts instantly, no network needed.
  • +
  • Earnings prediction → on-device per trip. Velocity forecasting runs locally after each trip completes.
  • +
  • Aggregation → server-side. Dashboard summaries, trends, and goal tracking are computed on the server where full trip history is available.
  • +
  • Why this split? A driver in danger can't wait for a network round-trip. Safety alerts must work in tunnels and dead zones.
  • +
+ +

Connectivity & Resilience

+
    +
  • Both ML models run locally — no network required for predictions.
  • +
  • Queued sync — trip data and feedback stored locally, synced when connectivity returns.
  • +
  • Rule-based fallback — if model files are missing, threshold rules (motion >3.5g + audio >80 dB → CONFLICT) keep the app functional.
  • +
  • SPA frontend — once loaded, all navigation works offline; only API data calls need network.
  • +
+ +

Battery & Resource Management

+
    +
  • Lightweight RF models — no GPU, <1ms inference, ~12 stress features (not hundreds).
  • +
  • 10 Hz sampling — sufficient for brakes/road events; 10× less data than high-frequency approaches.
  • +
  • 30s windows — model runs ~2×/min, not continuously.
  • +
  • dB envelope only — loudness levels, not raw audio waveforms. No FFT/spectrogram cost.
  • +
  • EMA drift correction — simple update every 10 min, not a full recalibration.
  • +
+ +

Privacy & Data Minimisation

+
    +
  • No raw audio — mic input reduced to dB aggregates on-device. No recordings leave the phone.
  • +
  • No raw sensor logs — accelerometer data consumed in 30s windows, converted to statistical features, then discarded.
  • +
  • All audio classification on-device — server never receives audio data.
  • +
  • Minimal PII — username + city + experience level. No phone number, no payment data.
  • +
  • Feedback is opt-in — only event ID + boolean. No free-text harvested.
  • +
+ +

Key Trade-offs

+
+ + + + + + +
DecisionGainedGave Up
On-device inferenceZero-latency alerts, offline supportCan't use large models or cross-driver patterns
RandomForest over DLLightweight, interpretable, CPU-onlyLower accuracy on complex temporal patterns
dB envelope over raw audioPrivacy, low CPU, no storageNo speech-to-text or fine-grained analysis
In-memory store over DBZero setup for judgesNo persistence across restarts
Feedback collected, not loopedSimpler systemModel doesn't self-improve (yet)
+ +
+ +
+ +
DrivePulse — Uber She++ Hackathon 2026 · Merin · Rishit · Lavisha
+ + diff --git a/docs/architecture_image.png b/docs/architecture_image.png new file mode 100644 index 0000000000000000000000000000000000000000..c1bf0ab517bf7014575dd5a92c39c8a4f6fb5007 GIT binary patch literal 244147 zcmeEuXH=72x2+(GSWrPlItm2oQbQFK1StUor5BYNfzW%2SU{vn?}QFYhtLTDK_Z0S zLJ6RB2)zn{M8%X_}@`Qwf|#{F}CB;(=9&VKfud+oi}Tx*74Ee+)hXBp0(IC0{F z$^%916DLkXPMn}{K69FMr?~R0+KCfn=3oT{En5X;1qV9^XC23<=9Y?J7e~9NURok2 zPTY$0dtav3qIIbashgATc8QsZNjO*`ChBaR1pKO^)0xN3%Q20)AKteqN8LOHBnG@B z2Au7NTnmztk~otYy*NLc{WWvWOE5V5rgy}Qzwdhej0Bq`#1XQ8GGDsU$hd{rMN_uP%Jldd}n7;18smL62qF`EeU;u#n88zus+CdJxuH2T&Inwgxj8%OkrGE`;8%BP&u(sHvpsaq=q7NQmdy*uj~1@y(O938@F1= zIo?ZA)bZcA6aIRUMQ8h@TiHB-AO)RTQ+&UpPSqIXSaV`dh+MU&RUp zI>AG_K68RB2z-KqbVWvbF_2y-PLQXQ|GaY=l78~%HHGt!4;5*U(I-yGpHNY}`^b}Q zWrEW8isfO`CRf;N7Pj}jKI?4mk4pCg5%CosJ??~EK5NMyX&uvP3-6>_qwljxy8chz89@3=uE%~@-u8N{?nTl zKRFIK`yk}n(r{xm0-m0o{YVt4^K8MhlYNv$(3$VmfBGV+N4L9dcP|PzA0j!DP!!wI z35z}~-J3z`Sex?P?Cfkl1KMs~gKK0Im*h|UhqrtMa^0YK1oM-3tKqjlPiszdaB_P0 z--@r{7H?0Qo~F^{t+@2J!M~8VAuC8qRGGCQ+>ZvT1YhNHJPKx9eVWB9Gz+pZHog0w z9{giOH?Q`FizG$#&4TiUpX(Ybt7TrjN*4_eew4KPpGQpk;zjbY03{dU&+{mL8nJpE zMX)H4asI}C1eo-J(B)ghsx|@#i5C13O8-LPI zrhHI^)9GNwywd}JS>hM+VrSpid{FXr5Btkvo{>2904pJup!7G)K0bWW3KE;Hu>H%T z$~SS#Pq%$&7X91GX@!wY2>_?(xBoi-M_DI=lb^R*WBvx^3!N9OVLGqCf17_g0}07T?04$_3S|ljfD{Jx zzs`T?mi%-s!{(^i-=O?T0$^tF$zSK6uS7y}=xtB&-=HMRJMq7!^#9M4(odheLPv-8 zn9_{uL0;!Qb?TJZ_Xjpo|8wcx^w?_GSfrH0f9~a0rS#HLtwzGUbbI$C2U0y1vO1f7 zGDrm-$IsDxgvq=w0-lgk8>?!MiiwHI7n@$jLE;4)8LaIcKdm$Gyt5zQXcUoH8 z%kA;*m?8TR_tmincz1h~_zx?cySR!u41L8>vWlvD3~jjF7%_z^@&aSI<=BoeyCA^Z z&uyi*8=V0POtJM)aQhK+kA?c7~@_5J%`<<%XTC)-6`SnF(e}>=U zwgcV{bqHQ2M;zJs>Rhu)`{PkLRQ=H zk0qY-h5VfIASB@R(aTT$V}5U$n12GKRY~sUWt6l3;?;ib03eL9{R0=PAf1_O_zrH| z2Zq*zH|;S?XX{P9%Fc$jB1c|3$i=3~)*Zm~P_19gHR9id#I3ksjt_UD>ep&i!lkx0 z`~(=g$%*)B#)Fp1>O(yhHMQ8QYF6K2A~&{jRQ61qcKM$^l-V4C&1IhJrnXIeKxbO^ zESAalI~7@hCgGdf61LX7Nv1>-ZkO0-g zNR+PL+~;D6+U}9n@kBa*{C%_y$_8MuGTZliTyag@vAX-+a>}9q!p-SssXdw%bu_-W;@5KiDY9BzXNK60)>_-ox_&iEXrU!j+dJA z*I!-u!-riuN2Z$d`ChF3uX|6=TuF2w${l{!iHeTqy*LTgyKZ}0;IKoj=L^c5z*Xv0Hun#S}z)5hdhSXg4%yfMjoSKXx6HscYmS*fa>bAA4*3;pH3Jo1| zAN@0G5PMAWSs+;#IOD%O#PgUY`n=RzEf-}UY;AAfNBJd)cra~nd5zLuKPN`EBI!zJSSpAO@k@TN)3;Wn3 zWI4HDo_0ly_)pOHVo#>Gh1L_Iv@_&*;J_K0%Vo6rVFW`{{`MH|@jAEU&eVE&tDZzb zw7=0eZiX+?>_sQAgXJv zBH{S+7TY2DLW*BqY>X4c03}j0e>i?W`YMC>%vK51N1V;cW!oMaHKkXwTjGwg2R_IP zU!5DmD5EjvD7EO^usL+kWVHQw6`E5I`_)W?i?H|oWK>^ft+D@57n$WqapTrA$+Z#o zlW{{_mfh&;#=Q}%P$_^RTPx3)#Srq_G$W@<%x znv*^YoJ5z@-mT{)$b&Sf>9}~&+<`UAzT6b1b#20DHr0b@+cs^$qZY>(?}o#u)mPbP zCv%HcPxan)?l#DKvx2yCA=`^5?br@axLxNj&3MX&VqBR2Iy-M)=~IQO?3|onQ*#uP?7ePb+3_TC zdzw;T(KOFpjNd7w^5S;|lX};v>d6E7Hs7r`g$DJLp&3JHsbZIIDU&2o@XAKkpVmJH#ORU@B~Cg|h{dZB{r!yNFM8xQQl5?wbU{k83L*~UpgK}_I+14H6O{|! z2EihQdrRy2RgX~x{X)*v5>N7tpS68)N(>cd0E6{k$2>TY41Cvp*L>pnsV>M1{HsQQu9 zyFxQ$bLi-!XJbj+_=}7;7hu4S20@Pv2SuVu?M9TXC1!1c;AI^l^|)Jl;XKjhIMrwV zfbO_QIsCc+(9|P`c%T`b*AcCGJU}$&B0O!I3wBNU@Z!QI#johCiL_nAc+ZLaj8p7INsDqX4 zwQJYn>Q!pZQ5s9bl@(VKF}Wa^rNh0X93X#ujX{M~YUsg^8c|rv2?x@{!)#wpC*g>< zi1;l=qD-oO^&KPsB;m)d*!R2w_3~@AxN9HG3ImCa?7@MbR&(}+>qMbLq*LybZlzaV}dSzLe;+r=afU%*#)NhoR(k0YJUPk-)j zwZ#|s)MkFlJb}+{Rdk`P)mKXOOI)n93j!z;Y-#*m)HOqbSZc=8>}8k3=IRMM@7)Hl z2cNKB=L%UWiQ%Rftw*A)|8r_9LbmuL|Oa!Vu=eM4;E zF~8%hBL#QRgCu@Tx_rcvVTmFfeKnh#!%Xf0pRU&J3cF+Xw?-Gt=U#c3aqmkXHV45v zSl1iO&W5}@!tt4vX*V8vH#BTbix9l}(t2%gZ~=i|5eu~2cU*}GXiRoChpk7FzTa#a z^h<$5DD?w*Tu4&W0pt^h*Ze5)%M@|%eTsVr)v(l|zZFc!HgE>I=j+!4S6G;E#v{s? z_r>=3a1W;_{Zoa|b?z6X91|LL$B@}iTTYXqAMHldi!6MRTrlI7c3(R`vdgfY_F>(C z=(TjgYjtkn$m~R~B3Mtu9{6+@`ft=@oVpE5+%USO}MV49JL_o zA&)LfCJD`-oG!qS=e@pKL`ciMpG4>R>lXn2mkP*ZCZ0(@qB*o5Feu!-T^avM#6Lk| zlxK84WxuPMHbs+@w<4Cw`%A#E45G}xzV-uS3f!q!OWW|7p9?`S;&?(*x;U@WZUSs= zYAgVz#o8bqdRNlV8Z)ot7XkJ*?;qwPQ1Ja|5%3H)Zm-xApGuUnHbC2IQdW<=M?8e< zDY?Dg8>`tTDDz+|Cr9M)4ZMmz@~vu}NwPg0DN6O?%F&mqr@YLXY_}H#qsG^31lhpm zg73|=p3tB#et{@ET)@Q^f79nzrwU#03KgtG)a3)}b;-TF_@*lCa;DC}5H5eNAv0r_nEjKV9oB zzi&9*MvvpIp(?Zt6@^obk>&Df;P>oP3+gm%ogb*_k7fWRD__To>@Z@wB_=}Kqiwu3 zZDh8w(86+sJ={{R=wQo5EITW7qYmq{Z=X4bUhzB*+voV6n|^GVugvdujcFD1pG7jA$(}a^>GZi3Nd25)fiDaJ2jmDgu1G zYN;>nLU*r_@8QnUQp(k6FtbPtJ+nHEx`X5MPpSx0za@i$qKSHs%C__1!TgL$PdEsE zGO-c>&Z@UmlI!4rpfxuzEmqNqq(tHE@B<; zH%`oh`A5o#=p#f3j&t|7n*2NrD z!us94WgqXbZige4-^7{tY{mjpLloWWvbbw==C1DUH&NqdZ;KSf#%A|9HM*|wf*y>{ z^}&+-a@7-5*F30ZYjVn+;VSSExFps-nZf&CtCccWe%kD1#_nf?rVY!1Io!96UkP-;UCW+E*Aw;g~8#XlG{7cr0_!ObjY5t%~S7l zHL;KCkB4zbU4zXll?$jGQj89xlgW6Mi`;-=URvW}I0>z7s{`+k2i9xm_n=%UPTx*h zA4H2wnNfOR()bOB(bOg=4Qz@Z;p&_>_Xv7vrk#t0SFlkl&e5?^cXVkcoPJ4@Z?UrA z)sQqTu+EQpK!p}a3au}GpTJ-S$63sjU{n6%ZbBgJm4r!wQwNr^rmnC(pY%pn*EMY4 zVc$WL=n#v>n31ASaKnN76YbH&C%tos*Ws8eNu^s$0sE<=jehKF(%ZNg=wwyEP1;uq$=+IkKtSZb-Wv z2q}+)iBIU=v5#Z#`zW|#?aeVtrEYtd5cD%2z;=n2l;0A1YMJ%(ba~+s@Ksx@2-NdJ zD`VgNhat$%FCH+X(ivbuyyhncaR-oOg#ZbJpvymNeS(i?v=vHHowTE%w=V@npjgh{Ai z!}mJh{p0Kx`>#6eM8M zg)H7vQ=V>+{4{Q_t131Z&oEOQ74=!3Y{7X-Ce=9kM;oEn7r%?UuZ%`JXnOk(GPWA} zciD2^V$OT^C7sN9iF4tU?6TP7@uf%flYtzSp;TLI`45@ERyc}PyGTo3e zh3O?C)tm*s*TB2RTzGWLzU(ZY3lC@ZZAVT|KdasPn2hF(U!6-%=m$5i!}e@VSt~83 zM;A=K2@1^{_beZnM}>9^Ii{5Oq&Du(+h%K)q!ovFEMG}jbT#?X*?Dm}cCbmPI|`w3 z3~$1@_N0}ksVfAXzrqab6aeK@(SzQBfK&|98X|MUrrvY*1sur$!OH#Fl-huqai*^C z;^K4>Kdo3^{S>(z_h(lm`_ zMl4qm4*J3JB<;T6jbwS5x9b`j?-|;NIO$+z>&1geEJRYe^BFbvr@8tybaEVp135GA z-FI}pHR!zO%by(koF=t9c~^=>$FWfKRhP5^T7C3KN-NF;5J1n9l6IG!QzN)ZF5 zHzb9xW|^P6{By}pkeAWj!Yf|E`;}4tc0&mAq+7jZk}JtJ;+=iU%$IdNMspFWhi|<&3JT+AOYYs{PbLMEP)^DRq)#Fvm`UZjm9sT32;P ze3FoytD^&gyRgjPugchIw<{AUuv&@=dRrAt%~UMsCgjR=!}>}h%Qe=B16Yd7Fqkm9 z?svOQZdE)311BglaB?=5+jsBo1|GYL*wizdMKB9uO#Kp}4eX$mBEzc*or_b*2t-W{ zr{a-dTc2VaH#*<^#9R`^J7!kj@4-yY8-zWbgLP$CEU)D}ROk~G$j~+z&gf8!S|0j_ z4c*3$&lHZ7Oj)XaH-WBRB{jwFygtXZLgTb2WjkDW?iN{|tTthP$%cqeXjC3CZydor z!ejy90ExERgQ3v7St__Vdj3KHI2gGOJ;Rg?ele3Y_y*pYxIL9}QI3FxO0-^AE8v_!-Pq1Y34K?#HBux% zfxT7vW+(TX0<>}5pJ7*-Isd3rL?bQ_c3fS#`ts8~uj4L{V?0<3*~kRNJ|rwiWyp#t zF*(^ZRZs3Ru9YCS8f=0^Kq)fc7{l z{ho-99-X{ohxz>~2v!RD1|m2!w>gb2K6fip+-vKRZPRyv6(8GiuJ^W87rA)!C{K&G z=>&i*>|I|pQfALXMg9Clfhwg~HKj|5I&Gc%l0}DO&gmE!e*0AfnnaQd)9B3ncZ+;Q zb64h_E%z)-3KlT*-rN49Tzx-)S4}F>wvI%^g_k>j1SA8wJyCd8Ph$b(3b{drd#cPv zuh~g&3$RxiZG_xd?Wih9FBU1T9sNj7orm!|;nhXC*m*#r3s#7|$f-2#2qnks#<$l& zXY5SDg!EW{E{N#RTfS{VG6zLXGr^%!7TB0HnY37)$H4EdxbABZu5OsDNN))7h694 zIR&Y57bfEKuflPFlt8*HRb%p{I_tNS4gPyesBHW3VWC(Cw?h>|YuK92n=I@A)qGop zQE&xPW`_%>gd20i6;&p$PSj!@Gg-NZ0GT3 zj^``A7JWgiP^;s`2DG58nW>*2{J4Ap#aMz@ke^m^Fx{tjEZw=C^I|wDdApeuRUU9i ze;jCqDDbfo%^~`FS4dl2X<7*lobYXVOd#uXgLx|)5-5qwQ+`>27qYy!8z;hvSzh8Dsv+g7OfU>Z)U1H>PVa$H!|8qg}nn7O=ptle#@%PMvw4_VzkX(1)o z&H=6jW-A5ltt$Fl6%avOO3i3jvDV~Uu>FH^Dw?tFU=}lxf>m#S1&Uv9rk|XI9*p|T zk9R89G%!pUm~>r`WVUYO?-oX!$KFG0`Rz30`T_ZN1{!Nt(0(P%0As^G)-7ZgDXl)e5l(kNX7CKMHM=2=5wxmT0f6?lVj% zC6CPDNQJH!QyKRt&k+}20Sg&d4p)7wMInP<7(9Je;#oVw=Ew1U)S#$_{cbhqm)tYC zCsDSGz*bGkB?+(j4=9e$G1?)bJM;4kum*NYW(oBqirB~fW$dCtI|p1@bASO}jv7~e zw-d4{qC6#yP^KHRjAC9kxO33LkqKT9gU5if)BJ(R)cbcz0zHul;_^HU{Nv7 zxith{ywAxplRoL|(4?3G<;57=;<`tTtoy-_y&tMwI;~vqZa-VlGga*`x9dBSv3@e* z&Ji@Xdg9a`9|m=89?Ntg^pqD|RwcLYcAuOnH(~ zeAHmOflM!jFT11Ce&2mw_actRh?MPi21O*h=fabz9Zer+OvRYhlMaj1AWItYjIsI~ zK3R)hcn#)pBg;4U_~~SjQlOXP+5<4$3tTXuHWN5u&ipy|bw=EY!GMWM>!7$O=X1=1 zccguy#R4DriCl{@-S1{O3&9NzdOeS<_o}e@B$}4l8|qSNvE(p(U2zq3Pub`G8`?wZ zy0K8}?2KUc>hkxajKFrK?SV1{JcE<}uB<+)l3PF^Qry0r+?(r-c}w~4SZnjf`{ZQn zJCp~Fg%73yce<&H{w5WX;`yqM)#51%H>F?_|7Ehj&W2w~lvWr?3G!W<3x4L8XZV>u z_zm82KB4O#cdmKTE;N`HqmzeoJ9D>eej$+b>NX59a`hx|f-O-q@iVHxbB?Rokafr|Xm zk3=uh)lD>ES=bM_F8HEq{^21bnAubE1VVyuS7qA(cGyRg%<4SyLwHZ#te$ zE>Z|b=%(WTm3{vA!39Y1kc2hiX`a+SYr_BDhKyoblTVFOD|B*ql)$&A0b3xTc>d_DEbs?#aRkRT}9GREw>FT|piberk)rAs#1ZKBI z$WfZ$I2@t(sw1t%@%(L9qU@($ypKWt6#IFh^9u}T+?Y50^$&1o6wYUD*5{A2`I0k= z$9v{}d2ULmcNO#Vll3e?eRV(jR#<|rvv0u>X55Y)&tt>oQg&yC(#scaGo3s?0gbr5 zLomIzyie+-rS|ka6Xgb0U4qo7OD#4>0?Xnbg`cI>7{J|X`9$R+718i{XPbiKj-rp_ zSh%%I?EyqYW8G1`c2v^Jq{N;fO|sB>2-bn&;nV9=_V_g_JUW0&bRfh>W&F-$(*NBNiX95q) zgVJWkuJ3_fd`)gN&ztj+GByz`@OoZrX)P$Ylk&Z0cI0qwxMx!PW<}i9xa4*5nUSX- z=4e>I70Trsy*t0c@V!jiq=BKPsAP4g?oQy%>vfEd%vym23T zTwf1bTMs&{zP~YbynMCb%+WdN4j*oQ{@D2sOP3ugv*C*GRMF6{y$7Z(Zhlne$5ENi z_P40a{Y=DbF5I_DHf^!@E5R*0UuQTO@A4w!3?^fJY{Q$s#zgu0GaE!t;HJY%Q;}vO zV6k#etcKce-Q;@Rk^Pl6w^lcsV)kmJpb(q@**WyLppQ$+p{3!t%gWJr_$(FxyT=*V z0Fj=IAGm1O-TKbo6K#`p^O;O13RZeAUXYKExGpa-?qr0l$vzTS5G%~DX@%d|7;+rO zSiTxhAKzhs;CxK<;#;&R6g*BAE5Jf9Q(VtY!Wg4t+m^CS}rS(Z2nMF6*|OX^n$E(hCA@(TG0P z$oQ}kq&X=uV93p0lqgz4XL^&1z36C5(q{|H_)QpN?`P796X&UKTtnM4n(nif;B4y~ zSYC~9237bsFnG!mv{o;oPAKe?tQtXNac2Jb0`Jzk?mSmQvhu;Hk`7bKA%wpKstJi+ zThD`)5KQThACA*VG*dAW1CoNbl~3;_wRdm(NArpxd_TZDic9Rb>n>Cu|6lH;nQtCt z<(O}a>*PK|sG9-N$;n*&hTpetJXFDs%=H@PipQTcx*)u8gQu2F7Dn->*B?9)z&Wnl zFqna@PL)HCcT2Z3Dvv&X-T1>n<};HwQ>EB& zIHUw8Z=H&jt&4j+KO}tNv(~k0YP!%wCBOP{mr{X2E#5 z>r>zLo~r+kv9Nu;`M!cnQ=D#Ar37HYA(XLV1QrNs>nfa{*5>A>!e?n?;jqA8uAeju z`Jpow3$b66wna+S-jT%XiVZr}AK<0uM5WP;HcPKU;X7{`KfU{7x@=nbQ#&HB2r&sn z8jijrMZ(kEkMIX$k77mfe~8u7tT>XGqifbmhxvG*>jR`#s1D!40^$`r!J!- z(mPeb@fG)^|Fm-h^28-e?)XtOh<^S|mK`sP_>+$hee~bm|DzE}f!<5bukF{K?78e_ z4rLdpFwkd>yvNE>@((*ny;3TtGXfL3dG`!XhsxYl>0YPQAH!k$dgaRlUPcUn3mUSW z^Be{l;>#Etmp}REgVN{EO33LbCN*q|PUre3B*n;fMJ@mLM*cAWvCsL>#guQlacL=t z8wE&E{KFQ0s3kAyMkMF2#$Na{&X!NTYGYbrmy#?M(iHG#yf0sL#b)mM0-*ood1gV7f?7K?7UL?h?_?svh&tp7PajH-%p+Ee*;TFA}3Vj-?;xV zGF>)yeiQ2FlKN4!(VXm+26i6vPPn87ZoZwG+aZ5qa)SH@#SZ$3GPO$NY_?)w4}By> zaPose{XfnlG728f^!DW0*DHf!$ zfaLkFe(C1@fl2%Gwd0VdwY6UTnfL2Br~Qfy&whFOCxr4tXKnheI&u?*xqXsmXB|F_ zrB(p{F=Vzt@+3IDTvqR)bArDLUlfT01~cIP+&I5o*uSoXE|D6Yd?X(HZ=d|JofL19 zI3SJK@E=F&KcW1QC3wd6e#J7`(8Ca?Y0EU|TxyYhaQe$;xd8gF&p+cyn~n+ta>f0H zUXuD2`1%0_>NIMmzLfu39^#)?IE82*ySJdUn*G%ISk0OW&x8xfDeh1V+wnxM@N*8I#=B`m4aU4)v`^vx z1-bxvrGQg~#klH$J^pz+?}!!TEAYMeeBfTwJc2{=M*G~x{o$3dC;TOXfJ94bh?qW2 zC$w2e2(KqnFqq_LQ;-praA}hDQyok)nGU^!J?Jlc$hb6^Uo@)wQd9$ci(h*n$FGGC zbG%ILab+!vK6$|iU%Uu4Vf1)kbwRl3zCTvNL{-%fs=ueQ4xs#pe+IxCcjkn!%QpEa9AzE-7@1SH#AeX;h5wU%iD_PsfUnLwwRc@&lSYtbzsPZ`#NS7h zRtUv>(XFWa1tFXsM!}+x_iW5Yob6-?9gflCnef69Exr){{fXX^x8G`>?s7a`N2NOr zoXfUhEpWAu=8zqGGXj3g3^vLY;E7>fxT*@YJ+`ogffxY~3U(Bi`lZ^Z0>6Un#tnGn zx&+UsnXGxOQ>*lIqcOz}{`|M0y!dPM{jvta$+gfxqw-w+xRPyVN2O>A`eEMBhTy}f zo1+N}<4t6U=o~Ne{oP7?oom-R0gk z)R+Q|LP6{Enw7Z+zx==@-hgss`LoI(p0VQamhH}$K+B2Z6=xqiizs$IcR95~AnxH> zp zIc%%}mg^ULnjxz?sV@$&3ER@_S@q47bK17;mPxF)EUWA-vkyX#riKe5Uy|y7kIm3G zqj!2GDq9{BTDv@O(6~{+jVvc?B(DiA5LQIcXw(9+B6w=4SboH~O za`v0ln!c)ry-LB_(m@Wj#w5c_r6GLLXKKFYi(+2C0cfQic@vStn65KzY@bBvblK`^T*_xw^e&6Rg{ZXknIN76ZJ#xQs zoc+z5W(d=r+n>j*m7<$oN4V5OR{Ojz^wStKW8-=R`C>Ie(@I%6DlaaqUHdKs-QJ@) zK#$j}N2E0M0QR;n8e@B5XUfE%5fyk@u0XEAWr>ey3N)nk`F>2gn;1Kutd zpjUNuZDhf@&y)?d)8RR^!*p=N$MBK3pFEn3uxdkt;&;EYDh%K~5?qBntO|!(y$u#r ze>OcZh2)39`{uf;J98mP?8F3O(u%;I--|P4iK;y;)oWZtaq6y}Ni`HwJAu};djN>@ zhex?Kt1wPH{!n({HK6Y0Oekn;+s_c|1W@q#+stcCxl~Lz;Q*8kZM47L<2w1Px||0@ z0hK5)#1~~`vz}CMI`%CI-d6<~f6Q|ciO`up(j~O{HZZH*UmblCr2{3d3mk04H@()l zk9)K;b|zY)%sfhEy5`MTms<_YP`Qc*6 zc|W-VE%^SsK)~0PG@V1=5S20P#>(D{>@dKal@SY?XVr*O1vus*pMuYw<7eJR6X4<* z=6XMSkkCPn!r>~Osp~eCoHTgg6G!u*+gWw>`xO(A)xlNU>7_5tjkXFgZZU?g zstW#p%`xAEybK?b_bvs_6gx?Qael;L_-;90oKh_Ukg9(1b(rvHEC;OIDR=2W>sAk+ zFM3&anpbCjvdP5CXT9kAz2ar3a6I|L1Rr^HfOFE)L?&=gZ{1i>t-45ZW8_Aur+UykBAeQ*Uf-F0lXX09qLZbqb7=;GHniDDrC= zzMEEW>?^BO|9aR`rTZ%X&081Yyw)$bT8Z#Wa*v&Dwcrh~+ArO5cun=wCfLGHd5f>| z1*lCJHmd=0eQ2cBP+ett6Oa0(TW+&*BDz3|MM2GjJLyt5P)%?*>Jn z%LQS3BYm@qrD1Vjc{b-R7nVO=HS|`)9df6AQhv)|fPN!T3JEt%4XJYz}VjE30 zyu0B+gGjk4{_3!;SSxDCPD{LajK4_-l<%Ul9=d0=j5sOmu;;=rSUtK!(|6rw&n13o zGm~>_IJpW8u3Wdi`gYIeU^oVa>+41+Yv)M?~J%!}3-SuBOl zi`hxV@W$W2*={C3%qiv20qPR(gmT<8;*FC1Y?B(C8`R+)o!(~yhJ;T&iy|9yQAF7; z*|xd&(SQ|14h>2ck|$YV38ptQGQB=JIPRN?cf}o8g~sc@2|t+y+!B;(`0(?80aO+a z1@Ke0-#5{W4XICC8&5sR-s*OMsRgLi)ZC9!iOB{Gxg7VEJzaSq5*&Kh%8peU>pjtV zduDYcL*=YshGpL!)7>!WL;eFd@1U%r?DyfAEUK(=yeTA1C+i%Tz9dt7-@zd#NeFz8 zIRr|1RsE>Z9I+y5zGAajBXu>lWMEb)->)_doIQSiUuMaP;4z1(`*_uQwQ}j%m{qKp zNIQRCS4kUS&}(nZ)TCSE3%I9Zx84tV&YjWo7tfr2NTOI1hnbHBw>ht5J_{(xyZU80 ztT7v8yPdKA4b9Mg`6MoQPo~$&2(CDmaqMTN*l$WTc9P>!HV4;(WA8P&jN!g=T?me1 z%J^cXfBu2JFinw@zLXWf0NCUpQPha_RWfj%fx*_QXh>y6+fPkJAgP{F0sDG@&Wa#`l)lt_V9w1%`$*&xU>o>F|CE{?x;gr(BX_t6A(23mABZnd6=LW9SBIUaxi_g1d@&x z8gO1XZRZd%it-g%3ukWWOb|7Fmex`drXs^UFyHH3P$@dIn}V6+kj$94EPN9uKMGMD z`h4>m#AI#C&SRkEBTr`Lb~a{ML&sku3-n6Y+3rlT52Z1$$1d>|_IXq&r;m@!Hv}gG(al+LEkx{f$(+VT zeR(d{1RgJGzjok)d!`V=&ZG>9drMHcG(Onjjl)y3YDqb`uNWBEc z$c)hv9lP2PrW;Q4nh~TTBMmPce;U5!DkTspBFb%P)+2Q~ip89d{Sj$(l4h4CyQ_%} z%sDsI=-SHt?0%LAm&4^}xnUAQW2MrlsVM3T4B(^lzFlOj^zdPXPj}pB!X_B5{1!uV)wH4VGJ84o8Jm#tXzpC=f%4S? zG$LCz2ivW=@siQqVXNmkh7C;;>y?q%pI$ZJ03K`;(g-PdokF)PQ#AE>IE^&mKx3pi z`#x^4_MXJ*P@0juL{Efl#P2L}KoW1MmCh(=t@tj2dnN06KigeHaZn7%NKL(mD$=c% zb58Z;D7p=w#gY{c&djiEQ|$2W)EDU@x*A_&HUmg#cZThXP=hWc6d_a?l7vj)K;k{Z3kl0-D+5Kyjy^~iG0YYCnb$ukb>QbZCKUtY=rgFPfJ3c<382Y-#o!0$r8Du$h zL8?*!HYnlzK=$BixFCI|d8hHli1;Yd`rY~EI6GA+*dgAJb=#+{&bLl$mQ>D(n*6dF z+mtoDgq1pkIbW=30dNlinDEe1b?S zb@q}c^*HAPDmem39pHDJ4d2Ywzl?Xl zl=2;v7C9n%x)?gBy72cy*U&rXgF@48}yJ?C$}h5-@-d{QPc6f(4B7p$i39OcXse> z*q-iLYLMfuJNIQ84WHy}(Jcpe+YEf=s@aQQp~ML?Ju>Eq>@OCw@WQ(duc@Dx%tYAO zEb*;6UE-0lI-~&}9$Ja8A?P&_*i2US5q8)b;_jO7rs04{D~soZq*>~dBTuRAY z3BkqIoXE$|JipTZ4ce@3bMc1G*tm;?!)tta*3m~sb@DbAv$lheYrGOpS+ZL2n3O&7 zzVA+uQGJ1EW)(8)9;OBHiQLrHW#e|neZ#Xo{O}dM4Nn;e+J02pKddnh;@wLT1SlqW zx}WsIkIN2AsMM>ItNA!BMeMoLTXc9Bo2%x6dor{(4=2lfUz%a;^MKA~>q8|TMz9RA zr&XSAk$fgbaZkodl)u`a0VRPiax!};`(Qc^O>g0MAC##^g2ZPgN6JcA9!Ba14rtgb zS14K1ZC}IfcS4wd>#$uLOM}g(blEW?nMs{tt`gTdg^yQIlpH1Jwdf|8DwMCydA1sqOK7Y1^+Ku5rlcG8# z2HWP(u<`xN`*B4dSI-O9RKSHYNesrx*!3vstIeYJO^x_IXRtd-{%H5q5`$i zXR3tv;)~85cNtrCPFS}67RVAbUL(qVqx$-JZ-smUo@K9tuGPyqC(g zAn78IHEvdEo~quHs0YzI?)OP(1=Wt`7EO*bk`kqnF?>kM`QNFpksp~k>8|}SB70DF zD`=}tGVnow%GWDbVh!h#R@|jocTd+c8}_&Ji#BW9R9$A0l6y2_`7xy=Z|EGhty<*l zY+{5T=c`oo(hj;B80pTBhPYEAS&p!^NG;UUFemf25-RFyVduKgy~R>mATmM7OhC?5 z5fzx&SZy5vxE;8j1q^aA>voqM#~HWb;Nh(15y{iWD6h5D?N@oJD%q~e+Re4thq;(A z24A@Xc@pt`!l~`r$-Meh*u@~yn7HD8Fg_HP?GaSogX!K?sMU>mkAL!VeZReyS+?6q zS?qn|U0H~0;^kMk_QlP{o)yEJnxtVA30{*H%E4&mvsmxlxmk9;I+djD2o-lYcO+Xn z-u%nGUAjQFkL=7Wo8uV?+`9#~@HSkzZJr;KRCaH8b!>4=C+m@_Wf-@tl`<*qwP(>} z{(&&}{FzONzi_&jV4^t<^Vs%X^t+rSiO0Gd^i? z2p%F@gwd}`Vm!o!-Ys5I$(B`EX?+=fVSw*-(0G-bk$d4}?POZ$;%4q7or>l_qm5#@ zO5x)xsA_3r4ST4?{SZ%o{y{I*i>2Js@pVsSFS;|jF)_%}F@SKfyK!Y!E5~LeCy<@V zTvjK(y1M;KA*raqX04f~E@bwi|9YBar_1Q1%}PDMUco2Ch8}KTui6z~0(VXDp%Oj5 zUhz!9K&A2;sDn#vr^d~3@J6npuH&P@ikW`%fsWYre#Y6dcUR}F5X@s0DE>oN^(vYR zkdoRi#*zDKFF6y^9yB_sh$m^=CRZPLdn|4xJ@|5_Ot5-&scNNn_MYWER092zF*@+d z@fq797MaE{m*gbTK8kRHjbQPT+Phi5fs?kI%g+V|)Mu9>+$nc5os03`U?z>=dE+Ka zDfCWyU%8**aCi6sh+cXEX~ZOS`!lR2u}U9#+tAEk%$#b}sm7d%$;WiV{6{iNBxfZn zz7{S2%l_BMN9P^VH${A*Xd5qs<79U1+atkN$Nei=bEJLK5{f+-9u{k!)_rEXRP{K@ zq2nXirsupM;)%eD^Sh&|;i*E4E^Dj8WO%mdD1=6_St#Gj0X%_xykUo{4KJ7xgR75E z+anL?Ja}PqP@zyN{>|K+-nJrqyP+D|4NQXyckIYsl3KP$y<@hu$fK7-6;=0`%{QX+IxGjiq(vvZaI4`W{)Q03P2s|eB|NS8E{(%ncm z(%s$NA_5}a-QC?Ooty5G+;p>PxEoKr$Mb#nz4t#JSge>eYu3bXW?3y}vVIzO+XGvp zKDDdEb3E>?%qtq5RtdY)Iea*>w64n%OQtsYN1CTgux27-St8fJ>X*E==Z z=*)99kheASFwQWseImw&?qpeH#$L^e3C|@S|7}yLgGdg?_JPNlCh`#B9K}b@8v7?f zg`tZo(!MrO-6CFB%IaX#lWEp_oJ=0`n&FuQLa%gK9~Of}%X#@Yxv?Ap_RKJ6Q|yXE z^2(Vx(z!1iV0o(eu=`HIOB6 z)<{Ir8HW%^e>FpHs5QP%z$R}r>|4`IBk^eU;t5y;&?0T9ri^;k#?Ss4#mP8=iFt@s z5emPFP!_tUorN6gEFF3nbrOtz`=oZC=CcB}(i-D{XY>^=g3&LWkwek9$?x-8#lQXB zs*#EXeX+ZPRjQ+P#pRR%oCmMOg*JNyg@F(?n#nW1%%0zB4&}~Qey~ZTVT64j3aRL9 z#W&eZnKD+l8y*01bk{rGu{R);p8KFQWofcTM_q^(oD+Jqn)ulvj6S=(hZy9P=6H~d z6uO|=yrNbk`DkQWvy{eUoT#k{Vp%8?h}&Toi$ydcrc<7YwIbR7PdHDQ+;(`ZiU967 z!A`K5~#^cKg2eFr-}_U$`T`n8$~+fV=l5I$>;u3~+rG?^(Q;!CTjLgOr=g00YQF2 zpO!eryDqM6_wA+{<+;p^+*?fbO^@Qp6jb-_UNzLeUp2adv9mVOnVbc2%MzqtWm!_s z>D{cbJc#ekmNZXq7!B0LGWrZDJdp$TOyR@=zZG&g7){IQgkuAVIgduRkIe|Ivw{VxZHHG8*3gCsHG>0C<+uZGO=ZBFv$>@wRoE^v4VC`X_DAX4JePQI zKzpCN*eB@ye!iXX%;S&d3qIuhR`&yE!(2B)#R2WWf2jJ`=wmB1*+`PH$Q0WeTN6iK zPJtIp;!)mtEpecz1hZbV%D(#Xw0G(YCFEbpGtZ=vlOfK>g+0`0nEYlJTLWrhx`pCd z5Zthm>*D(RqLLbA8^d2?~j3HcPc5SQ+BVdD9 zjy;^zwtmtcm}y}3H^!Xhs~3`CTl@q~i+MG_=~*n5M90w>R zti*OW5sYCNx}$DG(4KbpR?go5$0KgD$SHW)c%r5TkWw+l4QECp7Vk9ad{VEXV&5)W z_JXsN_0&7^Y7i1YsyGu_vK^j4G#(vmdHR7VZBRr*rN`qAE3*Iv{&L-RGTUm*w@JeK zF+1-RgDduU*ehR2+hJ_l+{<;$Hr)Fk`#O42kk&!CT1POG`^Gimc&4&wTs2?SNi%s&37WGa zmrmD_<|~`0aE7*u3%5x*1fySK;>RlM*$a&pf<51u^g+pSy~UNg$yTL1tb zVsxHfGY6V2>nGN$u1^8@NQvNGAFF9HZl!_{o{n+_N8FJsZ2p ze#}k8I85f`khj(3N8xMt){h%eTQrtCn=h(Zo{(8PreuqudV?5F|JH^MBs&}mnWeFI zIzXp5n9e*O0sHn*hAgJoW8EUgA&~})Nb!ren&!QQ^H*$f5X!8wO$lE|zrk5oK+_Wf z9Hgz88D1^y^NJ*HPCg;Fuo`dFfURLOTqpj!?@?5DoJh3r&MHrQsvC7Xbq8WBy{!|? zgRQU1Vxt$sYfSn!EB(Y39L<(~EP#qqElTaI^((V&M->-eC)&F_6$_6ol)wXFm*n63+GMylBWHMUinu9L=PdiN3jl;n*~(CkB)d^Lofgm80L z8(Kmdq0Oa^0|+ZI4q}{$nelkc>&@yz9UA+Zgin}U8DKAsIzannG?IKxoy#4)u@szi zjb3{`OV(DI=jcO$hw|9!Z(0{^^=F`){}5BGpPrKDj!N>|#xwz2bd?tm6&TdPe&oIc z$>veNs}!YDF&$Dq!S9HGLtF=;erplDtCls+U0b*-a(vB8oXKy%HjzN zV-jz2g{z%cZ&<#VJDV47w&Ev;gw8<@Yd^5DShAwJ0KdHkH#oeT!0O&=A=p={sisuhNPYyQn{1ZwXv$4E-fner zQS`ntUl-3A`9m&!isY|*FUjQ{a4bpKha`zk3w1zd?{J>;5niX3$;QHU-W-osarNTX zRI43bJei7G5Ac4(_pk zv`Myv^`$}^>+Z+K4`=1E*;c!oTG5++2puWnybf)4_$WEEJ1Rg6tz;&fU&JW_`Ue{2 z1;iZ&mXm6csVq(C*8X?0u8RVqEQ$(GuS&eJMHO>dNi@{I1$hZLqoG?p;jmAWf9Psg zRx4O)a%dZDJWbbjZM*)3-tkREZttWC1uY1;mFobYcj zS^*Upv7WaP0xT`2H$3K$p14D@K3&jFkiWPrLC>gYD^=o4;dR7 zxs0{`W~}E4zSHzg;c;U$nv}ArT}$jD1;eu%l6BpFZ$k%FbuM9ekDx=5U=Wxqp?rSQ zj)n1vV<#?<{o8Q=S}{1j4vsl*rwfJsFBJGYXUIGVoYSLjPt5Tf5clst|1m(p4MbYs zFU&g_#F;cJ86U#D`htVy>4XE~x2gVXpm*Mo+?Ndl1FOXF((A^jGw7$2_Ud2$8;WU) z@ue(yI?ls*NdM%5|Cnok{Dcz!oEdahGe-GehA#+y&J0>t119`~-~P2Q{&#dxQ5#iC z%y7REuR!n_Yp0;D?$2eY^82>KfX@JtZo+TrI8H+R(X47Cin$dPIo5a|M1Nx|J6bS9 zmdPTA{w++w=rjXXb(a zD{AuFeEuS)unw4`FQn&*|0~%d!F&B}P8IgI#r}&V-`YOsGgvc%HMWysJ%vvSr0U^G zzc(=t?s#T<54X&ixbI*fH>rB`zgiSYb||ik;N}k`XH9hOnN!^T4$u7sIgYc`APRyF znkY6?*{>FHB1|H;VP>`y`hBY-;hwR^E$BT_5$_l)EWR7EAU_4`t(Ctqv-lmBi84nW zk(^GE46^Kf_1o$SoFL{kB)T8%RY{>m|9=p2;N5y%V2S!$C%>;B<(qJa!}>FB)s)u! zZ|V52Hh+AAi-mwy>7<&(7#?Zx?J0gn&I4``Jw%{UHr^{@GRC> z5_o>6)xY>64V?1ks;^J z{mE)CU^r<0IZ%N(xMTB)`K;1^%xVha8Gy{u_y8j=BUT6{+C}vH0*szzz5QeVCvowh zmY-^y7IC$H|G9_%w+->7gaK#cK@cbZuf+dL8f6jj7cz%`e=Z4ix)^q$SV?1L>fPz;tZ*8MyQEG@=jSi(xw8b z{N`@2meFe)Pp!c;s6?|?oz;3UHnrJ#TJH@KJ{VU^GFBn*%51vI-*InF+x=>f2%J+J zfpoa2Y#d!;0r?DOy}F6_lzGG$n6Flo81~^wax7A`MWc|3jMw(oAiqCROy#sj0@t3W zqPOi{3+yK>Ymr0ZI>;Eb38DRi&T{Zj>t-Rz^FzTo4LRLS?+aK8EfhkMwcqJT(?&oQ zU+b$$bK2CPKld<}Z%~ET?!&|3ZrVC%Q-WlRNZ*ib+A*Pl( z#(6%b1rol1JWN37tTqw2j!N0!a&J%Db{S0+xFF?#WgMm2NUIk^NcRY@%yQO8$*o)5 zBaA<5*N>x&Rhd0`KHBY07ke~e6M786Xx|wIgi$<^b@DIOH@$PqXGd3U zhKi8I)o|crEJqCmz>TDe$J1z6CeX-%zFbH?9@;_X4RdswdA<>{h;1L|P5O;6)6rtB zroV<7CP(#bm|S!Iz~iD}$?6WR|0F9t_Duno#mwQ+e>S2fD2gH(jPXtXO#bDv_2a6+ z->~O*3cgELWM`*2o>8ok5LE;aFjM9)rA_&+w`In!2Ac>Plk5A_3EiS{q_x^n*u5dw zh56C?Z_{#)rN6R)?=&O|+VDIcE7nAT`R6X*EkN9^WF0fyYX3(fufgqv_EW4ZlF@rt zaxE#TJy#4VV1iJ@c&mOK>%`%f%_c(VbH1clBAeDhAJ*PzC^Y$7Aa} zeQKE$jKAbgR&&^%Z9E6OB>BvL55MCP-6s0x4&x-IVLWF9oLZ-hzqgRolE`lNz2)gg z{vC$Fa{w?S3gT8f`Lvi_m;@YBAzHa9

9KuDCFkW)a1%75uw1qS`*2@bo)2l13i<~f+D2F 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ŇxV`#kZotDsEn3)=pRn*jE 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⪚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 + + + + + DrivePulse — Design Document + + + + + +

+ + + +
+ + +
+

01 Product Vision

+

Who it's for, what it does, and why it's built this way.

+ +

User Persona

+

+ Full-time ride-hailing driver in an Indian metro city (Bangalore in our demo). + Drives 8–10 hours daily. Cares about two things: staying safe on chaotic roads and + hitting a daily earnings target. Not tech-savvy — needs information delivered at a glance, + not buried in charts. +

+ +

Core Value Proposition

+

+ DrivePulse transforms raw trip signals — accelerometer, speed, audio dB — into two actionable insights: +

+
    +
  1. Safety alerts — Detects dangerous situations (hard braking + loud cabin, escalating conflicts) in real-time and tells the driver why each event was flagged.
  2. +
  3. Earnings forecasting — Predicts earnings velocity (₹/hr), shows whether the driver is on track to hit their daily target, and estimates time-to-goal.
  4. +
+ +

The "Glanceable" Test

+

+ A driver can't read a dense dashboard while navigating traffic. Every screen in DrivePulse is designed to pass the 2-second glance test: +

+
    +
  • Dashboard — 5 large summary cards at the top. Green/yellow/red colour coding. No mental math needed.
  • +
  • Today Timeline — Horizontal hour blocks colour-coded by stress. One glance = "how has my day been?"
  • +
  • Earnings Progress — Single progress bar with current ₹/hr vs required ₹/hr. Status: 🟢 ahead / 🟡 on_track / 🔴 at_risk.
  • +
  • Event Cards — Emoji + one-line message + confidence badge. No jargon. "🚨 Hard brake + loud cabin at the same time."
  • +
  • Stress Tips — Dismissible amber cards with actionable advice. Not data — actions.
  • +
+ +
+ Design principle: The driver should never have to interpret raw numbers. + Every metric is translated into a status (ahead / on_track / at_risk), a colour (green / yellow / red), + or a plain-language message. Detailed signal charts and explainability modals exist for the + Trip Detail page — but they're opt-in, not the default view. +
+
+ + +
+

02 Stress Detection Algorithm

+

Lightweight signal processing, sensor fusion, and false-positive reduction.

+ +

Pipeline

+
+
Raw Sensors10 Hz
+
HALDevice Norm
+
30s Window~12 features
+
Z-ScoreBaseline μ/σ
+
RF Classify→ 7 classes
+
Platt Cal→ true prob
+
+ +

Cleaning Noisy Motion Data (HAL)

+

Phone sensors are noisy and device-dependent. Our Hardware Abstraction Layer runs a two-phase calibration:

+
    +
  • Phase 1 — Audio baseline: 30s stationary recording. Uses 10th-percentile dB as ambient floor, computes mic gain correction (target 52 dB). Normalises mic sensitivity across phone models.
  • +
  • Phase 2 — Road baseline: First 2 min of driving at >20 km/h. Detects gravity axis, computes accelerometer scale factor, and measures road vibration baseline. Subtracts road noise from real events.
  • +
  • Drift correction: EMA update (α=0.15) every 10 min adjusts audio floor and vibration baseline.
  • +
+

After HAL, accelerometer output = net motion above road vibration, audio = net cabin noise above ambient. Device-agnostic.

+ +

Interpreting Audio Intensity

+

Audio features are acoustic aggregates only — no raw audio stored (privacy by design). Key features per 30s window:

+
    +
  • audio_db_mean / p90 — overall cabin loudness
  • +
  • sustained_max — duration of sustained loud episodes
  • +
  • cadence_var — speech-like cadence variation (arguments = irregular cadence; music = steady)
  • +
  • audio_leads_motion — did audio spike before a hard brake? (escalation signal)
  • +
+

The insight: arguments have irregular cadence + sustained loudness, while music has regular cadence + steady levels. These two features alone separate ARGUMENT from MUSIC_OR_CALL well.

+ +

Multi-Sensor Fusion

+

Dangerous situations are rarely single-sensor. The key fusion features:

+
    +
  • both_elevated — motion AND audio above threshold simultaneously → strongest CONFLICT predictor.
  • +
  • audio_leads_motion — audio spikes >5s before hard brake → ESCALATING (argument → panic brake).
  • +
  • audio_only — high audio, no motion anomaly → ARGUMENT_ONLY or MUSIC_OR_CALL, not a road event.
  • +
+ +

Reducing False Positives

+
    +
  1. Platt calibration: Sigmoid recalibration so 85% confidence ≈ 85% true probability. Alerts only on HIGH (≥75%).
  2. +
  3. Confidence gating: If top-class confidence <50%, system defaults to NORMAL — no alert.
  4. +
  5. Class weighting: CONFLICT and ESCALATING weighted 3× during training (favour recall on dangerous events); Platt step corrects the resulting overconfidence.
  6. +
  7. Rule-based fallback: If model files are missing, simple thresholds (motion >3.5g + audio >80 dB → CONFLICT) ensure the app never crashes.
  8. +
+ +

Output

+

Per 30s window: situation class (7 classes), calibrated confidence (HIGH / MEDIUM / LOW), top feature deviations (for explainability), safety flag, <1ms inference on CPU.

+
+ + +
+

03 Earnings Velocity Algorithm

+

Current speed, forecasting, and cold-start handling.

+ +

Pipeline

+
+
Driver Statetrips, hours, ₹
+
Feature Eng14 features
+
RF Regress300 trees
+
₹/hr + Status+ probability
+
+ +

Current Earnings Speed

+

Earnings velocity = cumulative earnings ÷ elapsed hours (₹/hr). Required velocity = remaining earnings ÷ remaining hours. The gap between the two gives an instant "am I on track?" signal.

+ +

Feature Engineering (14 features)

+
+ + + + + +
GroupFeaturesWhy
Currentcurrent_velocity, elapsed_hours, trips_completed, trip_rateHow is the driver doing right now?
Contexthour_of_day, is_morning/lunch/evening_rushDemand varies by time of day
Historyvelocity_last_1/2/3, rolling_velocity_3/5Recent trend (momentum)
Goalgoal_pressure (target − current velocity)How urgently driver needs to speed up
+ +

Forecasting

+

RF regressor predicts next-period velocity. From that we derive:

+
    +
  • Forecast status: predicted > required → ahead · |diff| < ₹20 → on_track · else at_risk
  • +
  • Goal probability: clamp(1 − |predicted − required| / required, 0, 1)
  • +
  • Time to goal: remaining earnings ÷ predicted velocity
  • +
+ +

Handling Cold Start

+

First 15 min of a shift — no velocity lags, unstable trip rate, no rolling averages.

+
    +
  • Backfill: Missing lags set to current velocity (conservative — assumes no trend until proven).
  • +
  • Rolling windows: Use whatever data points are available, then backfill remaining NaN.
  • +
  • Goal pressure as anchor: Even with zero history, the model knows target vs current velocity — a strong directional signal from reading one.
  • +
  • Static profile features: avg_earnings_per_hour, experience_months, rating are available from registration and provide baseline expectations.
  • +
+ +
+ Edge case: A driver with a ₹5,000 target and 2 hours left sees required = ₹2,500/hr — clearly unreachable. + The model predicts honestly; the dashboard shows 🔴 at_risk. We don't hide bad news. +
+
+ + +
+

04 Execution Strategy

+

MVP scope, phased build, and the cut line.

+ +

MVP Scope

+
+ + + + + + +
LayerWhat's included
ML — StressLightweight RF classifier, ~12 key features, 7 classes, Platt calibration, HAL, rule-based fallback
ML — EarningsRF regressor, 14 features, velocity prediction, 5 derived goal metrics
BackendFastAPI (25 endpoints), in-memory store, batch processor, CSV import, auth
FrontendReact SPA (8 pages, 16 components), maps, charts, explainability modals
Judge toolsPredict page (single-row testing), Batch Upload (CSV inference + charts)
+ +

Phased Build

+
    +
  1. Data & Models — Feature engineering, train both RF models, validate with cross-validation.
  2. +
  3. Backend API — FastAPI with core endpoints, in-memory data store, batch inference.
  4. +
  5. Frontend Core — Dashboard, Trips, Trip Detail with map + charts, auth flow.
  6. +
  7. Polish & Judge Tools — Predict, Batch Upload, explainability, Trends, Goals.
  8. +
  9. Docs & Deploy — Design doc, architecture diagram, README, deployment.
  10. +
+ +

The Cut Line

+
+
+

What we cut:

+
    +
  • Real-time phone sensor streaming (simulated with synthetic data)
  • +
  • Persistent database (in-memory = zero-setup for judges)
  • +
  • Push notifications / WebSockets
  • +
  • Model retraining from feedback
  • +
  • Payment integration / real fare calculation
  • +
+
+
+

Why:

+
    +
  • Each adds setup complexity without demonstrating core algorithm thinking
  • +
  • Architecture supports a DB — we just don't require one to run
  • +
  • Feedback collection is built; retraining is a production concern
  • +
  • Time focused on model quality + explainability + UX
  • +
+
+
+ +

Key Trade-offs

+
+ + + + +
ChoiceGainedGave Up
RandomForest over deep learningLightweight, interpretable, CPU-only, <1ms inferencePotentially lower accuracy on complex patterns
In-memory store over DBZero setup, instant startupNo persistence across restarts
Synthetic sensor dataFull control over class balanceMay not generalise to real sensors
+
+ +
+ +
DrivePulse — Uber She++ Hackathon 2026 · Merin · Rishit · Lavisha
+ + From 4e9f8a3ef0474bdee0845e34fbd5dc3074cf0ab5 Mon Sep 17 00:00:00 2001 From: RishitGG Date: Tue, 10 Mar 2026 09:59:40 +0530 Subject: [PATCH 10/13] Docker --- Dockerfile.backend | 32 ++++++++++++++++++++++++++++++++ Dockerfile.frontend | 22 ++++++++++++++++++++++ README.md | 20 +++++++++++++++++++- docker-compose.yml | 21 +++++++++++++++++++++ nginx.conf | 19 +++++++++++++++++++ 5 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 Dockerfile.backend create mode 100644 Dockerfile.frontend create mode 100644 docker-compose.yml create mode 100644 nginx.conf diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..60a5521 --- /dev/null +++ b/Dockerfile.backend @@ -0,0 +1,32 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Copy dependency manifests +COPY requirements.txt . +COPY backend/requirements.txt backend/requirements.txt +COPY earnings/earnings/requirements.txt earnings/earnings/requirements.txt +COPY drivepulse_stress_model/requirements.txt drivepulse_stress_model/requirements.txt + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt \ + && pip install --no-cache-dir -r backend/requirements.txt \ + && pip install --no-cache-dir -r earnings/earnings/requirements.txt \ + && pip install --no-cache-dir -r drivepulse_stress_model/requirements.txt + +# Copy source code +COPY backend/ backend/ +COPY earnings/ earnings/ +COPY drivepulse_stress_model/ drivepulse_stress_model/ + +ENV PYTHONUNBUFFERED=1 + +EXPOSE 8000 + +# Match local dev layout: run from /app/backend so imports like `from utils...` +# and `from data...` resolve the same as `cd backend && python main.py` +WORKDIR /app/backend + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] + + diff --git a/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 0000000..7a206bf --- /dev/null +++ b/Dockerfile.frontend @@ -0,0 +1,22 @@ +FROM node:20-alpine AS build + +WORKDIR /app + +COPY frontend/package*.json ./ +RUN npm ci + +COPY frontend/ . +RUN npm run build + +FROM nginx:1.27-alpine + +WORKDIR /usr/share/nginx/html + +COPY --from=build /app/dist ./ + +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] + diff --git a/README.md b/README.md index a106ec5..779006b 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ flowchart LR - Python 3.9+ - Node.js 18+ -### Install & Run +### Install & Run (local dev) ```bash # Install Python dependencies @@ -104,6 +104,24 @@ Open **http://localhost:5173** in your browser. --- +### Run with Docker + +With [Docker Desktop](https://www.docker.com/products/docker-desktop/) running: + +```bash +# From the repo root +docker compose up --build +``` + +Then open: + +- Frontend: `http://localhost:5173` +- Backend (direct): `http://localhost:8000/api/health` + +The frontend talks to the backend via `/api/*`, which is proxied by Nginx inside the `frontend` container to the `backend` container. + +--- + ## Tech Stack | Layer | Tech | diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9a0de62 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +version: "3.9" + +services: + backend: + build: + context: . + dockerfile: Dockerfile.backend + container_name: drivepulse-backend + ports: + - "8000:8000" + + frontend: + build: + context: . + dockerfile: Dockerfile.frontend + container_name: drivepulse-frontend + ports: + - "5173:80" + depends_on: + - backend + diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..3024d03 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,19 @@ +server { + listen 80; + + # Serve the React SPA + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri /index.html; + } + + # Proxy API calls to the backend container + location /api/ { + proxy_pass http://backend:8000/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} + From c70aef446c8d087ac4edbe196d074b964671124e Mon Sep 17 00:00:00 2001 From: RishitGG Date: Tue, 10 Mar 2026 10:09:38 +0530 Subject: [PATCH 11/13] documentation update docker --- README.md | 8 +++++- docs/PROGRESS_LOG.md | 2 +- docs/architecture_explanation.html | 41 ++++++++++++++++++++++++++++++ docs/design.html | 39 ++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 779006b..637ccbc 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ flowchart LR ### Prerequisites - Python 3.9+ - Node.js 18+ + - Docker Desktop (for judge-friendly containerisation) ### Install & Run (local dev) @@ -109,7 +110,7 @@ Open **http://localhost:5173** in your browser. With [Docker Desktop](https://www.docker.com/products/docker-desktop/) running: ```bash -# From the repo root +# From the repo root (Driver-Pulse/) docker compose up --build ``` @@ -120,6 +121,11 @@ Then open: The frontend talks to the backend via `/api/*`, which is proxied by Nginx inside the `frontend` container to the `backend` container. +**Judge login (demo account):** + +- Username: `judge@uber.com` +- Password: `hackathon2026` + --- ## Tech Stack diff --git a/docs/PROGRESS_LOG.md b/docs/PROGRESS_LOG.md index 8e472ec..c5631a3 100644 --- a/docs/PROGRESS_LOG.md +++ b/docs/PROGRESS_LOG.md @@ -17,7 +17,7 @@ A chronological log of our problem-solving process and major iterations. **Mar 9:** Documentation: wrote design doc (HTML), architecture diagram (SVG), and progress log. Re-checked all features, tested flows, and made final edits based on feedback. Prepared for deployment. -**Mar 10:** Deployment and submission: deployed backend and frontend, final bugfixes, submitted project. +**Mar 10:** Deployment and submission: added Dockerised deployment (FastAPI + React via Nginx, `docker-compose.yml` at repo root), verified end-to-end flows in containers, and documented judge login plus Docker run instructions in all README/docs before final submission. --- diff --git a/docs/architecture_explanation.html b/docs/architecture_explanation.html index 882c5fa..88a0b2d 100644 --- a/docs/architecture_explanation.html +++ b/docs/architecture_explanation.html @@ -64,6 +64,7 @@

🚗 DrivePulse

@@ -279,6 +280,46 @@

Key Trade-offs

+ +
+

03 Deployment & Docker

+

How a judge runs the full system in minutes.

+ +
+ Hackathon requirement: Include a working Dockerfile and docker-compose.yml + so a judge can run the entire stack with a single command. DrivePulse ships two services + (FastAPI backend + React frontend) wired together behind Nginx. +
+ +

Containers

+
    +
  • Backend container: Python 3.11 + FastAPI. Runs uvicorn main:app inside the backend/ folder so imports like from utils... and from data... match local dev. Exposes 8000 with all /api/* routes.
  • +
  • Frontend container: Builds the React SPA with Node + Vite, then serves the static dist/ bundle via Nginx on port 80. Nginx proxies any request to /api/ to the backend service.
  • +
  • Compose orchestration: docker-compose.yml in the repo root builds both images, creates a shared network, and publishes: +
      +
    • http://localhost:5173 → frontend (Nginx)
    • +
    • http://localhost:8000 → backend (FastAPI, e.g. /api/health)
    • +
    +
  • +
+ +

Judge Runbook

+
    +
  1. Prerequisite: Install Docker Desktop and ensure it is running.
  2. +
  3. Clone repo: git clone ... && cd Driver-Pulse
  4. +
  5. One command: + docker compose up --build from the repo root (Driver-Pulse/).
  6. +
  7. Open app: visit http://localhost:5173 in the browser + (no manual dependency installation required).
  8. +
+ +
+ Judge credentials: we provide a pre-populated demo account.
+ Username: judge@uber.com · Password: hackathon2026. +
+ +
+
DrivePulse — Uber She++ Hackathon 2026 · Merin · Rishit · Lavisha
diff --git a/docs/design.html b/docs/design.html index 9433c0b..7338047 100644 --- a/docs/design.html +++ b/docs/design.html @@ -71,6 +71,7 @@

🚗 DrivePulse

Stress Detection Earnings Velocity Execution Strategy + Judge Experience & Docker
@@ -275,6 +276,44 @@

Key Trade-offs

+ +
+

05 Judge Experience & Docker

+

How we designed the repo and Docker setup so judges can run DrivePulse in a few minutes.

+ +

Zero-setup runtime

+

To avoid asking judges to install Python, Node, or ML libraries locally, we ship a fully containerised stack:

+
    +
  • Backend: Dockerfile.backend builds a Python 3.11 image, installs all ML and FastAPI dependencies from the repo's requirements.txt files, and starts uvicorn main:app from backend/.
  • +
  • Frontend: Dockerfile.frontend builds the React + Vite SPA, then serves it via Nginx, which also proxies /api/* to the backend service.
  • +
  • Compose: docker-compose.yml at the root orchestrates both services and exposes: +
      +
    • http://localhost:5173 → Nginx + React SPA
    • +
    • http://localhost:8000/api/health → backend health check
    • +
    +
  • +
+ +

Judge run instructions

+

We repeat the exact steps (as required by the hackathon) in the root README.md and docs:

+
    +
  1. Install Docker Desktop and ensure it is running.
  2. +
  3. git clone ... && cd Driver-Pulse
  4. +
  5. From the repo root run: docker compose up --build
  6. +
  7. Visit http://localhost:5173 to use the app; optional backend health at http://localhost:8000/api/health.
  8. +
+ +
+ Judge login: we provide a pre-configured demo account so judges land in a realistic dashboard immediately.
+ Username: judge@uber.com · Password: hackathon2026. +
+ +
+ This approach satisfies the hackathon's Docker criterion: a working Dockerfile and docker-compose.yml + at the repo root, with a single docker compose up --build command to start the entire application. +
+
+
DrivePulse — Uber She++ Hackathon 2026 · Merin · Rishit · Lavisha
From 8fa090af19f5aee5615cc11793484353a7446479 Mon Sep 17 00:00:00 2001 From: RishitGG Date: Tue, 10 Mar 2026 10:13:14 +0530 Subject: [PATCH 12/13] deployment fixes --- frontend/src/api/client.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index 11078aa..887e60b 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -1,4 +1,6 @@ -const BASE = '/api'; +// Default to relative /api for local dev and Docker. +// In hosted environments (Vercel/Netlify), set VITE_API_BASE to your backend URL. +const BASE = import.meta.env.VITE_API_BASE || '/api'; // Single place to plug in auth headers, retries, error normalisation, etc. async function request(path, options = {}) { From 3c1ffddaac6c2ae36d373e0d7e43362d03f4d6ce Mon Sep 17 00:00:00 2001 From: RishitGG Date: Tue, 10 Mar 2026 11:15:15 +0530 Subject: [PATCH 13/13] Add Railway Procfile --- Procfile | 1 + 1 file changed, 1 insertion(+) create mode 100644 Procfile diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..0a0a6fc --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: uvicorn backend.main:app --host 0.0.0.0 --port ${PORT:-8000}