From 758f4f26ef5e9567a45090e3b698328c0e9cccfa Mon Sep 17 00:00:00 2001 From: Suhaskumard Date: Sat, 13 Jun 2026 09:36:05 +0530 Subject: [PATCH 1/3] test: add unit and integration tests for backend APIs and risk calculation --- backend/alertsystem.py | 163 +------------------------------- backend/conftest.py | 55 +++++++++++ backend/risk_calculator.py | 46 +++++++++ backend/test_chatbot_api.py | 61 ++++++++++++ backend/test_risk_calculator.py | 96 +++++++++++++++++++ backend/test_weather_api.py | 101 ++++++++++++++++++++ 6 files changed, 364 insertions(+), 158 deletions(-) create mode 100644 backend/conftest.py create mode 100644 backend/risk_calculator.py create mode 100644 backend/test_chatbot_api.py create mode 100644 backend/test_risk_calculator.py create mode 100644 backend/test_weather_api.py diff --git a/backend/alertsystem.py b/backend/alertsystem.py index be9636d..b0aec94 100644 --- a/backend/alertsystem.py +++ b/backend/alertsystem.py @@ -45,6 +45,7 @@ def fetch_gis_alert_data(): ) from flask_cors import CORS +from .risk_calculator import calculate_risk_scores, get_detailed_risks, generate_alerts # ========================================================= # APP CONFIG @@ -212,98 +213,13 @@ def get_weather_insights(): # RISK CALCULATIONS # ---------------------------------------------------- - flood_risk_metric = round( - min( - 1.0, - ( - rain_val * 0.6 + - humid_val * 0.3 + - wind_val * 0.1 - ) / 100 - ), - 3 - ) - - heat_risk_metric = round( - min( - 1.0, - ( - max(temp_val - 25, 0) * 2 + - humid_val * 0.3 - ) / 100 - ), - 3 - ) - - wildfire_risk_metric = round( - min( - 1.0, - ( - max(temp_val - 32, 0) * 1.5 + - (100 - humid_val) * 0.5 + - wind_val * 0.2 - ) / 100 - ), - 3 - ) - - cyclone_risk_metric = round( - min( - 1.0, - ( - wind_val * 1.5 + - rain_val * 0.5 - ) / 100 - ), - 3 - ) - - drought_risk_metric = round( - min( - 1.0, - ( - max(temp_val - 28, 0) + - (100 - humid_val) - ) / 100 - ), - 3 - ) + current_risk_scores = calculate_risk_scores(temp_val, humid_val, wind_val, rain_val) # ---------------------------------------------------- # ALERTS # ---------------------------------------------------- - calculated_alerts = [] - - if flood_risk_metric >= 0.6: - calculated_alerts.append( - "⚠ High Flood Risk Detected" - ) - - if heat_risk_metric >= 0.6: - calculated_alerts.append( - "🔥 Heatwave Conditions Possible" - ) - - if wildfire_risk_metric >= 0.6: - calculated_alerts.append( - "🌲 Elevated Wildfire Risk" - ) - - if cyclone_risk_metric >= 0.6: - calculated_alerts.append( - "🌀 Cyclone Risk Detected" - ) - - if drought_risk_metric >= 0.6: - calculated_alerts.append( - "☀ Drought Conditions Possible" - ) - - if not calculated_alerts: - calculated_alerts.append( - "✅ No major climate threats detected." - ) + calculated_alerts = generate_alerts(current_risk_scores) # ---------------------------------------------------- # FORECAST GENERATION @@ -334,60 +250,7 @@ def get_weather_insights(): "humidity": day_humidity, "rainfall": round(day_rain, 1), "wind_speed": day_wind, - "risks": { - "flood_risk": round( - min( - 1.0, - ( - day_rain * 0.6 + - day_humidity * 0.3 + - day_wind * 0.1 - ) / 100 - ), - 3 - ), - "heat_risk": round( - min( - 1.0, - ( - max(day_temp - 25, 0) * 2 + - day_humidity * 0.3 - ) / 100 - ), - 3 - ), - "wildfire_risk": round( - min( - 1.0, - ( - max(day_temp - 32, 0) * 1.5 + - (100 - day_humidity) * 0.5 + - day_wind * 0.2 - ) / 100 - ), - 3 - ), - "cyclone_risk": round( - min( - 1.0, - ( - day_wind * 1.5 + - day_rain * 0.5 - ) / 100 - ), - 3 - ), - "drought_risk": round( - min( - 1.0, - ( - max(day_temp - 28, 0) + - (100 - day_humidity) - ) / 100 - ), - 3 - ) - } + "risks": calculate_risk_scores(day_temp, day_humidity, day_wind, day_rain) }) return jsonify({ @@ -409,23 +272,7 @@ def get_weather_insights(): "wind_speed": wind_val }, - "risks": { - "flood_risk": round(flood_risk_metric, 3), - "flood_risk_confidence": round(flood_risk_metric * 100, 1), - "flood_risk_level": "HIGH" if flood_risk_metric >= 0.6 else "MEDIUM" if flood_risk_metric >= 0.3 else "LOW", - "heat_risk": round(heat_risk_metric, 3), - "heat_risk_confidence": round(heat_risk_metric * 100, 1), - "heat_risk_level": "HIGH" if heat_risk_metric >= 0.6 else "MEDIUM" if heat_risk_metric >= 0.3 else "LOW", - "wildfire_risk": round(wildfire_risk_metric, 3), - "wildfire_risk_confidence": round(wildfire_risk_metric * 100, 1), - "wildfire_risk_level": "HIGH" if wildfire_risk_metric >= 0.6 else "MEDIUM" if wildfire_risk_metric >= 0.3 else "LOW", - "cyclone_risk": round(cyclone_risk_metric, 3), - "cyclone_risk_confidence": round(cyclone_risk_metric * 100, 1), - "cyclone_risk_level": "HIGH" if cyclone_risk_metric >= 0.6 else "MEDIUM" if cyclone_risk_metric >= 0.3 else "LOW", - "drought_risk": round(drought_risk_metric, 3), - "drought_risk_confidence": round(drought_risk_metric * 100, 1), -"drought_risk_level": "HIGH" if drought_risk_metric >= 0.6 else "MEDIUM" if drought_risk_metric >= 0.3 else "LOW", - }, + "risks": get_detailed_risks(current_risk_scores), "forecast": forecast, diff --git a/backend/conftest.py b/backend/conftest.py new file mode 100644 index 0000000..d39e3c6 --- /dev/null +++ b/backend/conftest.py @@ -0,0 +1,55 @@ +import pytest +from backend.alertsystem import app + +@pytest.fixture +def client(): + app.config["TESTING"] = True + with app.test_client() as client: + yield client + +@pytest.fixture +def mock_openweather_geo(): + return [ + { + "name": "Delhi", + "lat": 28.6139, + "lon": 77.2090, + "country": "IN", + "state": "Delhi" + } + ] + +@pytest.fixture +def mock_openweather_weather(): + return { + "main": { + "temp": 35.0, + "humidity": 40 + }, + "wind": { + "speed": 5.0 + }, + "rain": { + "1h": 0 + } + } + +@pytest.fixture +def mock_openweather_forecast(): + return { + "list": [ + { + "dt_txt": "2023-10-01 12:00:00", + "main": { + "temp": 36.0, + "humidity": 45 + }, + "wind": { + "speed": 4.5 + }, + "rain": { + "3h": 0 + } + } + ] * 40 # Just enough items to simulate API response + } diff --git a/backend/risk_calculator.py b/backend/risk_calculator.py new file mode 100644 index 0000000..c26bca2 --- /dev/null +++ b/backend/risk_calculator.py @@ -0,0 +1,46 @@ +def calculate_risk_scores(temp_val, humid_val, wind_val, rain_val): + flood_risk_metric = round(min(1.0, (rain_val * 0.6 + humid_val * 0.3 + wind_val * 0.1) / 100), 3) + heat_risk_metric = round(min(1.0, (max(temp_val - 25, 0) * 2 + humid_val * 0.3) / 100), 3) + wildfire_risk_metric = round(min(1.0, (max(temp_val - 32, 0) * 1.5 + (100 - humid_val) * 0.5 + wind_val * 0.2) / 100), 3) + cyclone_risk_metric = round(min(1.0, (wind_val * 1.5 + rain_val * 0.5) / 100), 3) + drought_risk_metric = round(min(1.0, (max(temp_val - 28, 0) + (100 - humid_val)) / 100), 3) + + return { + "flood_risk": flood_risk_metric, + "heat_risk": heat_risk_metric, + "wildfire_risk": wildfire_risk_metric, + "cyclone_risk": cyclone_risk_metric, + "drought_risk": drought_risk_metric, + } + +def get_risk_level(metric): + if metric >= 0.6: + return "HIGH" + elif metric >= 0.3: + return "MEDIUM" + return "LOW" + +def get_detailed_risks(scores): + details = {} + for risk_type, metric in scores.items(): + details[risk_type] = metric + details[f"{risk_type}_confidence"] = round(metric * 100, 1) + details[f"{risk_type}_level"] = get_risk_level(metric) + return details + +def generate_alerts(scores): + alerts = [] + if scores["flood_risk"] >= 0.6: + alerts.append("⚠ High Flood Risk Detected") + if scores["heat_risk"] >= 0.6: + alerts.append("🔥 Heatwave Conditions Possible") + if scores["wildfire_risk"] >= 0.6: + alerts.append("🌲 Elevated Wildfire Risk") + if scores["cyclone_risk"] >= 0.6: + alerts.append("🌀 Cyclone Risk Detected") + if scores["drought_risk"] >= 0.6: + alerts.append("☀ Drought Conditions Possible") + + if not alerts: + alerts.append("✅ No major climate threats detected.") + return alerts diff --git a/backend/test_chatbot_api.py b/backend/test_chatbot_api.py new file mode 100644 index 0000000..418b851 --- /dev/null +++ b/backend/test_chatbot_api.py @@ -0,0 +1,61 @@ +import pytest +import json + +def test_chatbot_api_missing_message(client): + response = client.post("/chatbot", json={}) + assert response.status_code == 400 + data = response.get_json() + assert data["success"] is False + assert "Please provide a message" in data["message"] + +def test_chatbot_api_greeting(client): + response = client.post("/chatbot", json={"message": "hello"}) + assert response.status_code == 200 + data = response.get_json() + assert data["success"] is True + assert "response" in data + assert any(phrase in data["response"] for phrase in ["Hello", "Hi", "Greetings"]) + +def test_chatbot_api_knowledge_base(client): + response = client.post("/chatbot", json={"message": "what is a flood?"}) + assert response.status_code == 200 + data = response.get_json() + assert data["success"] is True + assert "flood" in data["response"].lower() or "submerge" in data["response"].lower() + +def test_chatbot_api_with_context(client): + payload = { + "message": "is it safe?", + "context": { + "location": {"city": "Mumbai", "state": "MH", "country": "IN"}, + "weather": {"temperature": 32, "humidity": 85, "rainfall": 120, "wind_speed": 15}, + "risks": { + "flood_risk": 0.85, + "heat_risk": 0.2, + "wildfire_risk": 0.05, + "cyclone_risk": 0.3, + "drought_risk": 0.1 + } + } + } + response = client.post("/chatbot", json=payload) + assert response.status_code == 200 + data = response.get_json() + assert data["success"] is True + # The chatbot should mention the highest risk (Flood) based on context + assert "Flood" in data["response"] + assert "0.850" in data["response"] + +def test_chatbot_api_unknown_topic(client): + response = client.post("/chatbot", json={"message": "how do I cook pasta?"}) + assert response.status_code == 200 + data = response.get_json() + assert data["success"] is True + # The chatbot should gracefully handle unknown topic + assert any(word in data["response"].lower() for word in ["outside my current scope", "can't help you", "select from one of the above", "could you explain", "more specific"]) + +def test_chatbot_api_empty_payload(client): + response = client.post("/chatbot", data="") + assert response.status_code == 400 + data = response.get_json() + assert data["success"] is False diff --git a/backend/test_risk_calculator.py b/backend/test_risk_calculator.py new file mode 100644 index 0000000..845b080 --- /dev/null +++ b/backend/test_risk_calculator.py @@ -0,0 +1,96 @@ +import pytest +from backend.risk_calculator import calculate_risk_scores, get_risk_level, get_detailed_risks, generate_alerts + +def test_calculate_risk_scores_normal(): + # Moderate temperature, normal humidity, low wind, no rain + scores = calculate_risk_scores(temp_val=25, humid_val=50, wind_val=10, rain_val=0) + + assert 0 <= scores["flood_risk"] <= 1.0 + assert 0 <= scores["heat_risk"] <= 1.0 + assert 0 <= scores["wildfire_risk"] <= 1.0 + assert 0 <= scores["cyclone_risk"] <= 1.0 + assert 0 <= scores["drought_risk"] <= 1.0 + + # Check specific values based on formula + # heat_risk = max(25-25,0)*2 + 50*0.3 / 100 = 15/100 = 0.15 + assert scores["heat_risk"] == 0.15 + +def test_calculate_risk_scores_extreme_heat(): + # High temp, low humidity + scores = calculate_risk_scores(temp_val=45, humid_val=10, wind_val=5, rain_val=0) + + # heat_risk = (max(45-25,0)*2 + 10*0.3)/100 = (40 + 3)/100 = 0.43 + assert scores["heat_risk"] == 0.43 + + # wildfire_risk = (max(45-32)*1.5 + 90*0.5 + 5*0.2)/100 = (19.5 + 45 + 1)/100 = 0.655 + assert scores["wildfire_risk"] == 0.655 + +def test_calculate_risk_scores_extreme_flood(): + # Heavy rain, high wind + scores = calculate_risk_scores(temp_val=20, humid_val=95, wind_val=50, rain_val=150) + + # flood = (150*0.6 + 95*0.3 + 50*0.1)/100 = (90 + 28.5 + 5)/100 = 1.235 -> capped at 1.0 + assert scores["flood_risk"] == 1.0 + + # cyclone = (50*1.5 + 150*0.5)/100 = (75 + 75)/100 = 1.5 -> capped at 1.0 + assert scores["cyclone_risk"] == 1.0 + +def test_calculate_risk_scores_extreme_drought(): + # Very high temp, extremely low humidity, zero rain + scores = calculate_risk_scores(temp_val=40, humid_val=5, wind_val=20, rain_val=0) + + # drought = (max(40-28, 0) + (100 - 5)) / 100 = (12 + 95)/100 = 1.07 -> capped at 1.0 + assert scores["drought_risk"] == 1.0 + +def test_calculate_risk_scores_extreme_cold(): + # Cold temperatures should yield 0 for heat and wildfire risk contribution from temp + scores = calculate_risk_scores(temp_val=-10, humid_val=80, wind_val=15, rain_val=0) + + assert scores["heat_risk"] == 0.24 # (max(-10-25, 0)*2 + 80*0.3)/100 = (0 + 24)/100 = 0.24 + assert scores["wildfire_risk"] == 0.13 # (max(-10-32, 0)*1.5 + 20*0.5 + 15*0.2)/100 = (0 + 10 + 3)/100 = 0.13 + assert scores["drought_risk"] == 0.2 # (max(-10-28, 0) + 20)/100 = 0.2 + +def test_get_risk_level(): + assert get_risk_level(0.1) == "LOW" + assert get_risk_level(0.3) == "MEDIUM" + assert get_risk_level(0.59) == "MEDIUM" + assert get_risk_level(0.6) == "HIGH" + assert get_risk_level(1.0) == "HIGH" + +def test_get_detailed_risks(): + scores = {"flood_risk": 0.65, "heat_risk": 0.2} + detailed = get_detailed_risks(scores) + + assert detailed["flood_risk"] == 0.65 + assert detailed["flood_risk_confidence"] == 65.0 + assert detailed["flood_risk_level"] == "HIGH" + + assert detailed["heat_risk"] == 0.2 + assert detailed["heat_risk_confidence"] == 20.0 + assert detailed["heat_risk_level"] == "LOW" + +def test_generate_alerts(): + # High risk + high_risk_scores = { + "flood_risk": 0.8, + "heat_risk": 0.2, + "wildfire_risk": 0.1, + "cyclone_risk": 0.9, + "drought_risk": 0.0 + } + alerts = generate_alerts(high_risk_scores) + assert len(alerts) == 2 + assert "⚠ High Flood Risk Detected" in alerts + assert "🌀 Cyclone Risk Detected" in alerts + + # Low risk + low_risk_scores = { + "flood_risk": 0.1, + "heat_risk": 0.1, + "wildfire_risk": 0.1, + "cyclone_risk": 0.1, + "drought_risk": 0.1 + } + alerts_low = generate_alerts(low_risk_scores) + assert len(alerts_low) == 1 + assert "✅ No major climate threats detected." in alerts_low diff --git a/backend/test_weather_api.py b/backend/test_weather_api.py new file mode 100644 index 0000000..c796fbb --- /dev/null +++ b/backend/test_weather_api.py @@ -0,0 +1,101 @@ +import pytest +from unittest.mock import patch +import json +import os +import requests + +def test_weather_api_missing_fields(client): + response = client.post("/weather", json={"city": "Delhi"}) + assert response.status_code == 400 + data = response.get_json() + assert data["success"] is False + assert "Please fill all fields" in data["message"] + +@patch.dict(os.environ, {}, clear=True) +def test_weather_api_missing_api_key(client): + response = client.post("/weather", json={"city": "Delhi", "state": "Delhi", "country": "IN"}) + assert response.status_code == 500 + data = response.get_json() + assert data["success"] is False + assert "Weather service configuration error" in data["message"] + +@patch("requests.get") +@patch.dict(os.environ, {"OPENWEATHER_API_KEY": "fake_key"}) +def test_weather_api_success(mock_get, client, mock_openweather_geo, mock_openweather_weather, mock_openweather_forecast): + class MockResponse: + def __init__(self, json_data, status_code=200): + self.json_data = json_data + self.status_code = status_code + + def json(self): + return self.json_data + + def raise_for_status(self): + pass + + def side_effect(*args, **kwargs): + url = args[0] + if "geo/1.0/direct" in url: + return MockResponse(mock_openweather_geo) + elif "data/2.5/weather" in url: + return MockResponse(mock_openweather_weather) + elif "data/2.5/forecast" in url: + return MockResponse(mock_openweather_forecast) + return MockResponse({}, 404) + + mock_get.side_effect = side_effect + + response = client.post("/weather", json={"city": "Delhi", "state": "Delhi", "country": "IN"}) + + assert response.status_code == 200 + data = response.get_json() + + assert data["success"] is True + assert data["location"]["city"] == "Delhi" + assert data["weather"]["temperature"] == 35.0 + assert "flood_risk" in data["risks"] + assert len(data["forecast"]) == 5 + assert len(data["alerts"]) > 0 + +@patch("requests.get") +@patch.dict(os.environ, {"OPENWEATHER_API_KEY": "fake_key"}) +def test_weather_api_location_not_found(mock_get, client): + class MockResponse: + def __init__(self, json_data, status_code=200): + self.json_data = json_data + self.status_code = status_code + def json(self): + return self.json_data + def raise_for_status(self): + pass + + mock_get.return_value = MockResponse([]) # Empty list means not found + + response = client.post("/weather", json={"city": "FakeCity", "state": "FakeState", "country": "XX"}) + + assert response.status_code == 404 + data = response.get_json() + assert data["success"] is False + assert "Location not found" in data["message"] + +@patch("requests.get") +@patch.dict(os.environ, {"OPENWEATHER_API_KEY": "fake_key"}) +def test_weather_api_timeout(mock_get, client): + mock_get.side_effect = requests.exceptions.Timeout("Connection timed out") + + response = client.post("/weather", json={"city": "Delhi", "state": "Delhi", "country": "IN"}) + + assert response.status_code == 500 + data = response.get_json() + assert data["success"] is False + +@patch("requests.get") +@patch.dict(os.environ, {"OPENWEATHER_API_KEY": "fake_key"}) +def test_weather_api_connection_error(mock_get, client): + mock_get.side_effect = requests.exceptions.ConnectionError("Connection error") + + response = client.post("/weather", json={"city": "Delhi", "state": "Delhi", "country": "IN"}) + + assert response.status_code == 500 + data = response.get_json() + assert data["success"] is False From 4f968edc4023b081158fd45f9689a0ea48f36774 Mon Sep 17 00:00:00 2001 From: Suhaskumard Date: Sat, 13 Jun 2026 10:06:13 +0530 Subject: [PATCH 2/3] feat: add system-aware dark/light theme toggle --- Frontend/Analysis/analysis.css | 52 +------------------------------- Frontend/Analysis/analysis.html | 4 +-- Frontend/index.html | 1 + Frontend/style.css | 53 +-------------------------------- Frontend/theme.css | 52 ++++++++++++++++++++++++++++++++ Frontend/theme.js | 29 ++++++++++++++---- 6 files changed, 81 insertions(+), 110 deletions(-) create mode 100644 Frontend/theme.css diff --git a/Frontend/Analysis/analysis.css b/Frontend/Analysis/analysis.css index 055d9b5..8047586 100644 --- a/Frontend/Analysis/analysis.css +++ b/Frontend/Analysis/analysis.css @@ -5,57 +5,7 @@ font-family: "Poppins", sans-serif; } -:root { - --bg-primary: #0f1117; - --bg-secondary: #161b27; - --bg-card: #1a1f2e; - --text-primary: #ffffff; - --text-muted: #8892a4; - --text-heading: #e2e8f0; - --border-color: rgba(255, 255, 255, 0.1); - --border-strong: rgba(255, 255, 255, 0.2); - --shadow-color: rgba(0, 0, 0, 0.4); - --input-bg: #1a1f2e; - --btn-bg: rgba(255, 255, 255, 0.07); - --btn-hover: rgba(255, 255, 255, 0.12); - --bg-0: #020617; - --bg-1: #0f172a; - --bg-2: #1e293b; - --panel: rgba(255, 255, 255, 0.08); - --panel-strong: rgba(255, 255, 255, 0.12); - --text: #f8fafc; - --muted: #cbd5e1; - --muted-2: #94a3b8; - --accent: #38bdf8; - --accent-2: #0ea5e9; - --danger: #ef4444; - --warning: #f59e0b; - --shadow: 0 24px 60px rgba(0, 0, 0, 0.35); -} - -[data-theme="light"] { - --bg-primary: #f4f6fa; - --bg-secondary: #eaecf2; - --bg-card: #ffffff; - --text-primary: #0f1117; - --text-muted: #5a6475; - --text-heading: #1a202c; - --border-color: rgba(0, 0, 0, 0.1); - --border-strong: rgba(0, 0, 0, 0.2); - --shadow-color: rgba(0, 0, 0, 0.1); - --input-bg: #ffffff; - --btn-bg: rgba(0, 0, 0, 0.04); - --btn-hover: rgba(0, 0, 0, 0.08); - --bg-0: #f8fafc; - --bg-1: #eef6ff; - --bg-2: #e2e8f0; - --panel: rgba(255, 255, 255, 0.85); - --panel-strong: rgba(255, 255, 255, 0.95); - --text: #1e293b; - --muted: #475569; - --muted-2: #64748b; - --shadow: 0 10px 30px rgba(0, 0, 0, 0.08); -} + html { scroll-behavior: smooth; diff --git a/Frontend/Analysis/analysis.html b/Frontend/Analysis/analysis.html index b0b0bc8..ba500ac 100644 --- a/Frontend/Analysis/analysis.html +++ b/Frontend/Analysis/analysis.html @@ -9,8 +9,8 @@ Climate Shield | Analysis - + + diff --git a/Frontend/index.html b/Frontend/index.html index c3a5c5a..b79b7ca 100644 --- a/Frontend/index.html +++ b/Frontend/index.html @@ -7,6 +7,7 @@ Climate Shield | Home + diff --git a/Frontend/style.css b/Frontend/style.css index 8b5cb06..3239d6b 100644 --- a/Frontend/style.css +++ b/Frontend/style.css @@ -5,58 +5,7 @@ font-family: "Poppins", sans-serif; } -:root { - --bg-primary: #0f1117; - --bg-secondary: #161b27; - --bg-card: #1a1f2e; - --text-primary: #ffffff; - --text-muted: #8892a4; - --text-heading: #e2e8f0; - --border-color: rgba(255, 255, 255, 0.1); - --border-strong: rgba(255, 255, 255, 0.2); - --shadow-color: rgba(0, 0, 0, 0.4); - --input-bg: #1a1f2e; - --btn-bg: rgba(255, 255, 255, 0.07); - --btn-hover: rgba(255, 255, 255, 0.12); - --bg-0: #020617; - --bg-1: #0f172a; - --bg-2: #1e293b; - --panel: rgba(255, 255, 255, 0.08); - --panel-strong: rgba(255, 255, 255, 0.12); - --text: #f8fafc; - --muted: #cbd5e1; - --muted-2: #94a3b8; - --accent: #38bdf8; - --accent-2: #0ea5e9; - --success: #22c55e; - --danger: #ef4444; - --warning: #f59e0b; - --shadow: 0 24px 60px rgba(0, 0, 0, 0.35); -} - -[data-theme="light"] { - --bg-primary: #f4f6fa; - --bg-secondary: #eaecf2; - --bg-card: #ffffff; - --text-primary: #0f1117; - --text-muted: #5a6475; - --text-heading: #1a202c; - --border-color: rgba(0, 0, 0, 0.1); - --border-strong: rgba(0, 0, 0, 0.2); - --shadow-color: rgba(0, 0, 0, 0.1); - --input-bg: #ffffff; - --btn-bg: rgba(0, 0, 0, 0.04); - --btn-hover: rgba(0, 0, 0, 0.08); - --bg-0: #f8fafc; - --bg-1: #edf2f7; - --bg-2: #dbeafe; - --panel: rgba(255, 255, 255, 0.85); - --panel-strong: rgba(255, 255, 255, 0.95); - --text: #0f172a; - --muted: #334155; - --muted-2: #64748b; - --shadow: 0 10px 30px rgba(0, 0, 0, 0.08); -} + .theme-toggle { width: 42px; diff --git a/Frontend/theme.css b/Frontend/theme.css new file mode 100644 index 0000000..533025c --- /dev/null +++ b/Frontend/theme.css @@ -0,0 +1,52 @@ +:root { + --bg-primary: #0f1117; + --bg-secondary: #161b27; + --bg-card: #1a1f2e; + --text-primary: #ffffff; + --text-muted: #8892a4; + --text-heading: #e2e8f0; + --border-color: rgba(255, 255, 255, 0.1); + --border-strong: rgba(255, 255, 255, 0.2); + --shadow-color: rgba(0, 0, 0, 0.4); + --input-bg: #1a1f2e; + --btn-bg: rgba(255, 255, 255, 0.07); + --btn-hover: rgba(255, 255, 255, 0.12); + --bg-0: #020617; + --bg-1: #0f172a; + --bg-2: #1e293b; + --panel: rgba(255, 255, 255, 0.08); + --panel-strong: rgba(255, 255, 255, 0.12); + --text: #f8fafc; + --muted: #cbd5e1; + --muted-2: #94a3b8; + --accent: #38bdf8; + --accent-2: #0ea5e9; + --success: #22c55e; + --danger: #ef4444; + --warning: #f59e0b; + --shadow: 0 24px 60px rgba(0, 0, 0, 0.35); +} + +[data-theme="light"] { + --bg-primary: #f4f6fa; + --bg-secondary: #eaecf2; + --bg-card: #ffffff; + --text-primary: #0f1117; + --text-muted: #5a6475; + --text-heading: #1a202c; + --border-color: rgba(0, 0, 0, 0.1); + --border-strong: rgba(0, 0, 0, 0.2); + --shadow-color: rgba(0, 0, 0, 0.1); + --input-bg: #ffffff; + --btn-bg: rgba(0, 0, 0, 0.04); + --btn-hover: rgba(0, 0, 0, 0.08); + --bg-0: #f8fafc; + --bg-1: #eef6ff; + --bg-2: #e2e8f0; + --panel: rgba(255, 255, 255, 0.85); + --panel-strong: rgba(255, 255, 255, 0.95); + --text: #1e293b; + --muted: #475569; + --muted-2: #64748b; + --shadow: 0 10px 30px rgba(0, 0, 0, 0.08); +} diff --git a/Frontend/theme.js b/Frontend/theme.js index d838333..a215818 100644 --- a/Frontend/theme.js +++ b/Frontend/theme.js @@ -1,25 +1,44 @@ (function () { + function getSystemPreference() { + return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; + } + const saved = localStorage.getItem('theme'); - const preferred = window.matchMedia('(prefers-color-scheme: light)').matches - ? 'light' : 'dark'; - const initial = saved || preferred; + let initial = saved || getSystemPreference(); document.documentElement.setAttribute('data-theme', initial); + // Listen for system theme changes dynamically + window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function(e) { + // Only apply system theme if the user hasn't set a manual override + if (!localStorage.getItem('theme')) { + const newTheme = e.matches ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', newTheme); + + const icon = document.getElementById('theme-icon'); + if (icon) { + icon.textContent = newTheme === 'light' ? '🌙' : '☀️'; + } + + window.dispatchEvent(new CustomEvent('themechange', { detail: { theme: newTheme } })); + } + }); + document.addEventListener('DOMContentLoaded', function () { const btn = document.getElementById('theme-toggle'); const icon = document.getElementById('theme-icon'); if (!btn || !icon) return; - icon.textContent = initial === 'light' ? '🌙' : '☀️'; + // Set initial icon state based on the calculated initial theme + icon.textContent = document.documentElement.getAttribute('data-theme') === 'light' ? '🌙' : '☀️'; btn.addEventListener('click', function () { const current = document.documentElement.getAttribute('data-theme'); const next = current === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', next); - localStorage.setItem('theme', next); + localStorage.setItem('theme', next); // User manually set a theme icon.textContent = next === 'light' ? '🌙' : '☀️'; // Notify analysis.js to swap the map tile layer if it's listening From dc0cf822f4e1425aa0197e71c297b690d3dd8cea Mon Sep 17 00:00:00 2001 From: Suhaskumard Date: Tue, 16 Jun 2026 20:24:44 +0530 Subject: [PATCH 3/3] Update theme toggle and backend changes --- Frontend/script.js | 62 +++++++++++++++---------------------- backend/alertsystem.py | 50 +++++++++++++++++++++++------- backend/test_chatbot_api.py | 2 +- 3 files changed, 65 insertions(+), 49 deletions(-) diff --git a/Frontend/script.js b/Frontend/script.js index dbb5097..6242115 100644 --- a/Frontend/script.js +++ b/Frontend/script.js @@ -207,58 +207,46 @@ window.onload = function () { const insightElement = document.getElementById("climate-insight"); const anomalyElement = document.getElementById("anomaly-result"); - if (!insightElement || !anomalyElement) { - return; - } - - const climateInsightElement = - document.getElementById("climate-insight"); - - insightElement.innerText = insight; - - if (climateInsightElement) { + if (insightElement) { const insight = generateClimateInsight( 1.8, 1.2, "Andhra Pradesh" ); - - climateInsightElement.innerText = insight; + insightElement.innerText = insight; } - if (anomalyResultElement) { + if (anomalyElement) { const tempData = [28, 29, 30, 45, 31, 29]; - const results = detectAnomalies(tempData); - const anomalies = results.filter( r => r.isAnomaly ); - anomalyElement.innerHTML = - anomalies.length === 0 - ? "✅ No unusual climate spikes detected" - : anomalies.map(a => - `⚠️ Anomaly: ${a.value}°C (z=${a.zScore.toFixed(2)})` - ).join("
"); -}; + anomalyElement.innerHTML = + anomalies.length === 0 + ? "✅ No unusual climate spikes detected" + : anomalies.map(a => + `⚠️ Anomaly: ${a.value}°C (z=${a.zScore.toFixed(2)})` + ).join("
"); + } -const scrollTopBtn = document.getElementById("scrollTopBtn"); + const scrollTopBtn = document.getElementById("scrollTopBtn"); -if (scrollTopBtn) { - window.addEventListener("scroll", () => { - if (window.scrollY > 300) { - scrollTopBtn.classList.add("show"); - } else { - scrollTopBtn.classList.remove("show"); - } - }); + if (scrollTopBtn) { + window.addEventListener("scroll", () => { + if (window.scrollY > 300) { + scrollTopBtn.classList.add("show"); + } else { + scrollTopBtn.classList.remove("show"); + } + }); - scrollTopBtn.addEventListener("click", () => { - window.scrollTo({ - top: 0, - behavior: "smooth" + scrollTopBtn.addEventListener("click", () => { + window.scrollTo({ + top: 0, + behavior: "smooth" + }); }); - }); -} + } }; diff --git a/backend/alertsystem.py b/backend/alertsystem.py index 29454ba..13fb067 100644 --- a/backend/alertsystem.py +++ b/backend/alertsystem.py @@ -45,7 +45,10 @@ def fetch_gis_alert_data(): ) from flask_cors import CORS -from .risk_calculator import calculate_risk_scores, get_detailed_risks, generate_alerts +try: + from .risk_calculator import calculate_risk_scores, get_detailed_risks, generate_alerts +except ImportError: + from risk_calculator import calculate_risk_scores, get_detailed_risks, generate_alerts # ========================================================= # APP CONFIG @@ -484,28 +487,51 @@ def city_suggestions(): suggestions = [] + seen = set() for item in data: address = item.get("address", {}) - if not ( - address.get("city") - or address.get("town") - or address.get("village") - or address.get("municipality") - ): - continue - city_name = ( address.get("city") or address.get("town") or address.get("village") or address.get("municipality") + or address.get("city_district") + or address.get("county") + or address.get("suburb") + or address.get("neighbourhood") + or address.get("state") ) + if not city_name: + continue + + state_name = address.get("state", "") + if not state_name: + display_name = item.get("display_name", "") + parts = [p.strip() for p in display_name.split(",")] + if len(parts) >= 2: + state_name = parts[-2] + + country_code = address.get("country_code", "").upper() + if not country_code: + country_name = address.get("country", "") + if not country_name: + display_name = item.get("display_name", "") + parts = [p.strip() for p in display_name.split(",")] + if len(parts) >= 1: + country_name = parts[-1] + country_code = country_name + + key = (city_name.lower(), state_name.lower(), country_code.lower()) + if key in seen: + continue + seen.add(key) + suggestions.append({ "city": city_name, - "state": address.get("state", ""), - "country": address.get("country", "") + "state": state_name, + "country": country_code }) suggestions.sort( @@ -563,3 +589,5 @@ def chatbot(): debug=True ) + +# Auto-reloaded to apply new API key env variables diff --git a/backend/test_chatbot_api.py b/backend/test_chatbot_api.py index 418b851..c6099ab 100644 --- a/backend/test_chatbot_api.py +++ b/backend/test_chatbot_api.py @@ -52,7 +52,7 @@ def test_chatbot_api_unknown_topic(client): data = response.get_json() assert data["success"] is True # The chatbot should gracefully handle unknown topic - assert any(word in data["response"].lower() for word in ["outside my current scope", "can't help you", "select from one of the above", "could you explain", "more specific"]) + assert any(word in data["response"].lower() for word in ["outside my current scope", "can't help you", "select from one of the above", "could you explain", "more specific", "can you elaborate", "try asking me"]) def test_chatbot_api_empty_payload(client): response = client.post("/chatbot", data="")