diff --git a/backend/__init__.py b/backend/__init__.py index e69de29..9e58ed7 100644 --- a/backend/__init__.py +++ b/backend/__init__.py @@ -0,0 +1,10 @@ +# Modular routers for VishwaGuru Backend + +# Fix for googletrans compatibility with newer httpcore (Issue #290) +# This monkeypatch must happen before any imports of googletrans or httpx +try: + import httpcore + if not hasattr(httpcore, "SyncHTTPTransport"): + httpcore.SyncHTTPTransport = object +except ImportError: + pass diff --git a/backend/cache.py b/backend/cache.py index 13c5aa1..032f60b 100644 --- a/backend/cache.py +++ b/backend/cache.py @@ -174,3 +174,4 @@ def invalidate(self): nearby_issues_cache = ThreadSafeCache(ttl=60, max_size=100) # 1 minute TTL, max 100 entries user_upload_cache = ThreadSafeCache(ttl=3600, max_size=1000) # 1 hour TTL for upload limits blockchain_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=1) +grievance_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=1) diff --git a/backend/grievance_service.py b/backend/grievance_service.py index 849f983..9d01a8c 100644 --- a/backend/grievance_service.py +++ b/backend/grievance_service.py @@ -5,11 +5,13 @@ import json import uuid +import hashlib from typing import Dict, Any, Optional, List from sqlalchemy.orm import Session, joinedload from datetime import datetime, timezone, timedelta from backend.models import Grievance, Jurisdiction, GrievanceStatus, SeverityLevel, Issue +from backend.cache import grievance_last_hash_cache from backend.database import SessionLocal from backend.routing_service import RoutingService from backend.sla_config_service import SLAConfigService @@ -84,6 +86,19 @@ def create_grievance(self, grievance_data: Dict[str, Any], db: Session = None) - # Generate unique ID unique_id = str(uuid.uuid4())[:8].upper() + # Blockchain chaining logic (Issue #290 optimization) + # Performance Boost: Use thread-safe cache to eliminate DB query for last hash + prev_hash = grievance_last_hash_cache.get("last_hash") + if prev_hash is None: + # Cache miss: Fetch only the last hash from DB + prev_grievance = db.query(Grievance.integrity_hash).order_by(Grievance.id.desc()).first() + prev_hash = prev_grievance[0] if prev_grievance and prev_grievance[0] else "" + grievance_last_hash_cache.set(data=prev_hash, key="last_hash") + + # SHA-256 chaining based on key grievance fields + hash_content = f"{unique_id}|{grievance_data.get('category', 'general')}|{severity.value}|{prev_hash}" + integrity_hash = hashlib.sha256(hash_content.encode()).hexdigest() + # Extract location data location_data = grievance_data.get('location', {}) latitude = location_data.get('latitude') if isinstance(location_data, dict) else None @@ -106,11 +121,17 @@ def create_grievance(self, grievance_data: Dict[str, Any], db: Session = None) - assigned_authority=assigned_authority, sla_deadline=sla_deadline, status=GrievanceStatus.OPEN, - issue_id=grievance_data.get('issue_id') + issue_id=grievance_data.get('issue_id'), + integrity_hash=integrity_hash, + previous_integrity_hash=prev_hash ) db.add(grievance) db.commit() + + # Update cache for next grievance only AFTER successful commit (Issue #290 optimization) + grievance_last_hash_cache.set(data=integrity_hash, key="last_hash") + db.refresh(grievance) return grievance diff --git a/backend/init_db.py b/backend/init_db.py index 21dd181..dbfbd63 100644 --- a/backend/init_db.py +++ b/backend/init_db.py @@ -166,6 +166,17 @@ def index_exists(table, index_name): if not index_exists("grievances", "ix_grievances_category_status"): conn.execute(text("CREATE INDEX IF NOT EXISTS ix_grievances_category_status ON grievances (category, status)")) + if not column_exists("grievances", "integrity_hash"): + conn.execute(text("ALTER TABLE grievances ADD COLUMN integrity_hash VARCHAR")) + logger.info("Added integrity_hash column to grievances") + + if not column_exists("grievances", "previous_integrity_hash"): + conn.execute(text("ALTER TABLE grievances ADD COLUMN previous_integrity_hash VARCHAR")) + logger.info("Added previous_integrity_hash column to grievances") + + if not index_exists("grievances", "ix_grievances_previous_integrity_hash"): + conn.execute(text("CREATE INDEX IF NOT EXISTS ix_grievances_previous_integrity_hash ON grievances (previous_integrity_hash)")) + # Field Officer Visits Table (Issue #288) # This table is newly created for field officer check-in system if not inspector.has_table("field_officer_visits"): diff --git a/backend/main.py b/backend/main.py index ef3414b..3d78164 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3,6 +3,15 @@ from pathlib import Path from dotenv import load_dotenv +# Fix for googletrans compatibility with newer httpcore (Issue #290) +# This monkeypatch must happen before any imports of googletrans or httpx +try: + import httpcore + if not hasattr(httpcore, "SyncHTTPTransport"): + httpcore.SyncHTTPTransport = object +except ImportError: + pass + load_dotenv() # Add project root to sys.path to ensure 'backend.*' imports work @@ -85,9 +94,10 @@ async def lifespan(app: FastAPI): logger.info("Starting database initialization...") await run_in_threadpool(Base.metadata.create_all, bind=engine) logger.info("Base.metadata.create_all completed.") - # Temporarily disabled - comment out to debug startup issues - # await run_in_threadpool(migrate_db) - logger.info("Database initialized successfully (migrations skipped for local dev).") + + # Run migrations to ensure schema is up-to-date (Issue #290) + await run_in_threadpool(migrate_db) + logger.info("Database initialized and migrations applied successfully.") except Exception as e: logger.error(f"Database initialization failed: {e}", exc_info=True) # We continue to allow health checks even if DB has issues (for debugging) @@ -126,9 +136,8 @@ async def lifespan(app: FastAPI): app = FastAPI( title="VishwaGuru Backend", description="AI-powered civic issue reporting and resolution platform", - version="1.0.0" - # Temporarily disable lifespan for local dev debugging - # lifespan=lifespan + version="1.0.0", + lifespan=lifespan ) # Add centralized exception handlers diff --git a/backend/models.py b/backend/models.py index b6466d0..1f7d5de 100644 --- a/backend/models.py +++ b/backend/models.py @@ -84,7 +84,9 @@ class Grievance(Base): created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc), index=True) updated_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc), onupdate=lambda: datetime.datetime.now(datetime.timezone.utc)) resolved_at = Column(DateTime, nullable=True) - + integrity_hash = Column(String, nullable=True) # Blockchain integrity seal + previous_integrity_hash = Column(String, nullable=True, index=True) # Linked hash for O(1) verification + # Closure confirmation fields closure_requested_at = Column(DateTime, nullable=True) closure_confirmation_deadline = Column(DateTime, nullable=True) diff --git a/backend/requirements-render.txt b/backend/requirements-render.txt index c6649b2..d774b38 100644 --- a/backend/requirements-render.txt +++ b/backend/requirements-render.txt @@ -22,3 +22,4 @@ googletrans==4.0.2 langdetect numpy scikit-learn +httpcore diff --git a/backend/routers/detection.py b/backend/routers/detection.py index 558b8b5..0181d26 100644 --- a/backend/routers/detection.py +++ b/backend/routers/detection.py @@ -459,7 +459,7 @@ async def detect_abandoned_vehicle_endpoint(image: UploadFile = File(...)): @router.post("/api/detect-emotion") async def detect_emotion_endpoint( image: UploadFile = File(...), - client: httpx.AsyncClient = backend.dependencies.Depends(get_http_client) + client = backend.dependencies.Depends(get_http_client) ): """ Analyze facial emotions in the image using Hugging Face inference. diff --git a/backend/routers/grievances.py b/backend/routers/grievances.py index 4d5566c..07f69a8 100644 --- a/backend/routers/grievances.py +++ b/backend/routers/grievances.py @@ -5,6 +5,7 @@ import os import json import logging +import hashlib from datetime import datetime, timezone from backend.database import get_db @@ -15,7 +16,7 @@ FollowGrievanceRequest, FollowGrievanceResponse, RequestClosureRequest, RequestClosureResponse, ConfirmClosureRequest, ConfirmClosureResponse, - ClosureStatusResponse + ClosureStatusResponse, GrievanceBlockchainVerificationResponse ) from backend.grievance_service import GrievanceService from backend.closure_service import ClosureService @@ -436,3 +437,52 @@ def get_closure_status( except Exception as e: logger.error(f"Error getting closure status for grievance {grievance_id}: {e}", exc_info=True) raise HTTPException(status_code=500, detail="Failed to get closure status") + + +@router.get("/grievances/{grievance_id}/blockchain-verify", response_model=GrievanceBlockchainVerificationResponse) +def verify_grievance_blockchain(grievance_id: int, db: Session = Depends(get_db)): + """ + Verify the cryptographic integrity of a grievance using blockchain-style chaining. + Optimized: Uses previous_integrity_hash column for O(1) verification. + """ + # Fetch current grievance data including the link to previous hash + # Performance Boost: Use projected previous_integrity_hash to avoid N+1 or secondary lookups + grievance = db.query( + Grievance.id, + Grievance.unique_id, + Grievance.category, + Grievance.severity, + Grievance.integrity_hash, + Grievance.previous_integrity_hash + ).filter(Grievance.id == grievance_id).first() + + if not grievance: + raise HTTPException(status_code=404, detail="Grievance not found") + + # Determine previous hash (use stored link or fallback for legacy records) + prev_hash = grievance.previous_integrity_hash + + if prev_hash is None: + # Fallback for legacy records created before O(1) optimization + prev_grievance_hash = db.query(Grievance.integrity_hash).filter(Grievance.id < grievance_id).order_by(Grievance.id.desc()).first() + prev_hash = prev_grievance_hash[0] if prev_grievance_hash and prev_grievance_hash[0] else "" + + # Recompute hash based on current data and previous hash + # Chaining logic matches grievance_service.py: hash(unique_id|category|severity|prev_hash) + severity_value = grievance.severity.value if hasattr(grievance.severity, 'value') else str(grievance.severity) + hash_content = f"{grievance.unique_id}|{grievance.category}|{severity_value}|{prev_hash}" + computed_hash = hashlib.sha256(hash_content.encode()).hexdigest() + + is_valid = (computed_hash == grievance.integrity_hash) + + if is_valid: + message = "Integrity verified. This grievance is cryptographically sealed and has not been tampered with." + else: + message = "Integrity check failed! The grievance data does not match its cryptographic seal." + + return GrievanceBlockchainVerificationResponse( + is_valid=is_valid, + current_hash=grievance.integrity_hash, + computed_hash=computed_hash, + message=message + ) diff --git a/backend/schemas.py b/backend/schemas.py index 7dd398e..6be9c5d 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -306,6 +306,12 @@ class BlockchainVerificationResponse(BaseModel): computed_hash: str = Field(..., description="Hash computed from current issue data and previous issue's hash") message: str = Field(..., description="Verification result message") +class GrievanceBlockchainVerificationResponse(BaseModel): + is_valid: bool = Field(..., description="Whether the grievance integrity is intact") + current_hash: Optional[str] = Field(None, description="Current integrity hash stored in DB") + computed_hash: str = Field(..., description="Hash computed from current grievance data and previous grievance's hash") + message: str = Field(..., description="Verification result message") + # Resolution Proof Schemas (Issue #292) diff --git a/backend/tests/test_grievance_blockchain.py b/backend/tests/test_grievance_blockchain.py new file mode 100644 index 0000000..7e7a080 --- /dev/null +++ b/backend/tests/test_grievance_blockchain.py @@ -0,0 +1,95 @@ +from fastapi.testclient import TestClient +import pytest +import hashlib +from backend.main import app +from backend.database import get_db, Base, engine +from backend.models import Grievance, Jurisdiction, JurisdictionLevel, SeverityLevel +from sqlalchemy.orm import Session +from datetime import datetime, timedelta, timezone + +@pytest.fixture +def db_session(): + Base.metadata.create_all(bind=engine) + session = Session(bind=engine) + # Create a jurisdiction which is required for grievance + jurisdiction = Jurisdiction( + level=JurisdictionLevel.LOCAL, + geographic_coverage={"cities": ["Mumbai"]}, + responsible_authority="Mumbai MC", + default_sla_hours=24 + ) + session.add(jurisdiction) + session.commit() + yield session + session.close() + Base.metadata.drop_all(bind=engine) + +@pytest.fixture +def client(db_session): + app.dependency_overrides[get_db] = lambda: db_session + with TestClient(app) as c: + yield c + app.dependency_overrides = {} + +def test_grievance_blockchain_chaining(client, db_session): + # Create two grievances through the API/Service logic would be better but let's test the chaining manually first then through service + from backend.grievance_service import GrievanceService + service = GrievanceService() + + # Reset cache to ensure clean state + from backend.cache import grievance_last_hash_cache + grievance_last_hash_cache.clear() + + # Grievance 1 + g1_data = { + "category": "Road", + "severity": "medium", + "city": "Mumbai", + "description": "Pothole in sector 5" + } + g1 = service.create_grievance(g1_data, db=db_session) + assert g1 is not None + assert g1.integrity_hash is not None + assert g1.previous_integrity_hash == "" + + # Grievance 2 + g2_data = { + "category": "Garbage", + "severity": "high", + "city": "Mumbai", + "description": "Waste overflow" + } + g2 = service.create_grievance(g2_data, db=db_session) + assert g2 is not None + assert g2.integrity_hash is not None + assert g2.previous_integrity_hash == g1.integrity_hash + + # Verify through API + response = client.get(f"/grievances/{g1.id}/blockchain-verify") + assert response.status_code == 200 + assert response.json()["is_valid"] == True + + response = client.get(f"/grievances/{g2.id}/blockchain-verify") + assert response.status_code == 200 + assert response.json()["is_valid"] == True + +def test_grievance_blockchain_failure(client, db_session): + # Manually create a tampered grievance + jurisdiction = db_session.query(Jurisdiction).first() + g = Grievance( + unique_id="TAMPERED", + category="Road", + severity=SeverityLevel.MEDIUM, + current_jurisdiction_id=jurisdiction.id, + assigned_authority="Mumbai MC", + sla_deadline=datetime.now(timezone.utc) + timedelta(hours=24), + integrity_hash="fakehash", + previous_integrity_hash="" + ) + db_session.add(g) + db_session.commit() + + response = client.get(f"/grievances/{g.id}/blockchain-verify") + assert response.status_code == 200 + assert response.json()["is_valid"] == False + assert "Integrity check failed" in response.json()["message"] diff --git a/render.yaml b/render.yaml index 6dc40b6..ba22a3a 100644 --- a/render.yaml +++ b/render.yaml @@ -31,6 +31,9 @@ services: value: production - key: DEBUG value: false + # Disable local ML on Render to prevent OOM errors (Issue #290) + - key: USE_LOCAL_ML + value: false # CORS settings - key: CORS_ORIGINS sync: false # Set to your frontend URL diff --git a/server_log.txt b/server_log.txt new file mode 100644 index 0000000..8fd26bb --- /dev/null +++ b/server_log.txt @@ -0,0 +1,53 @@ +2026-03-20 14:51:01,153 - backend.adaptive_weights - INFO - Adaptive weights loaded/reloaded. +2026-03-20 14:51:01,225 - backend.rag_service - INFO - Loaded 5 civic policies for RAG. +Starting server on port 10000 +Traceback (most recent call last): + File "/app/start-backend.py", line 13, in + uvicorn.run("backend.main:app", host="0.0.0.0", port=port, log_level="info") + File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/uvicorn/main.py", line 606, in run + server.run() + File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/uvicorn/server.py", line 75, in run + return asyncio_run(self.serve(sockets=sockets), loop_factory=self.config.get_loop_factory()) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/asyncio/runners.py", line 195, in run + return runner.run(main) + ^^^^^^^^^^^^^^^^ + File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/asyncio/runners.py", line 118, in run + return self._loop.run_until_complete(task) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete + return future.result() + ^^^^^^^^^^^^^^^ + File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/uvicorn/server.py", line 79, in serve + await self._serve(sockets) + File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/uvicorn/server.py", line 86, in _serve + config.load() + File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/uvicorn/config.py", line 441, in load + self.loaded_app = import_from_string(self.app) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/uvicorn/importer.py", line 19, in import_from_string + module = importlib.import_module(module_str) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/importlib/__init__.py", line 90, in import_module + return _bootstrap._gcd_import(name[level:], package, level) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "", line 1387, in _gcd_import + File "", line 1360, in _find_and_load + File "", line 1331, in _find_and_load_unlocked + File "", line 935, in _load_unlocked + File "", line 999, in exec_module + File "", line 488, in _call_with_frames_removed + File "/app/backend/main.py", line 35, in + from backend.routers import issues, detection, grievances, utility, auth, admin, analysis, voice, field_officer, hf, resolution_proof + File "/app/backend/routers/voice.py", line 27, in + from backend.voice_service import get_voice_service + File "/app/backend/voice_service.py", line 14, in + from googletrans import Translator + File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/googletrans/__init__.py", line 6, in + from googletrans.client import Translator + File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/googletrans/client.py", line 30, in + class Translator: + File "/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/googletrans/client.py", line 62, in Translator + proxies: typing.Dict[str, httpcore.SyncHTTPTransport] = None, + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +AttributeError: module 'httpcore' has no attribute 'SyncHTTPTransport' diff --git a/server_log_final.txt b/server_log_final.txt new file mode 100644 index 0000000..7e218d2 --- /dev/null +++ b/server_log_final.txt @@ -0,0 +1,29 @@ +2026-03-20 15:23:55,991 - backend.adaptive_weights - INFO - Adaptive weights loaded/reloaded. +2026-03-20 15:23:56,068 - backend.rag_service - INFO - Loaded 5 civic policies for RAG. +/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/pydub/utils.py:170: RuntimeWarning: Couldn't find ffmpeg or avconv - defaulting to ffmpeg, but may not work + warn("Couldn't find ffmpeg or avconv - defaulting to ffmpeg, but may not work", RuntimeWarning) +INFO: Started server process [16397] +INFO: Waiting for application startup. +2026-03-20 15:23:56,802 - backend.main - INFO - Shared HTTP Client initialized. +2026-03-20 15:23:56,803 - backend.main - INFO - Starting database initialization... +2026-03-20 15:23:57,078 - backend.main - INFO - Base.metadata.create_all completed. +2026-03-20 15:23:57,098 - backend.init_db - INFO - Database migration check completed successfully. +2026-03-20 15:23:57,099 - backend.main - INFO - Database initialized and migrations applied successfully. +2026-03-20 15:23:57,099 - backend.main - INFO - Initializing grievance service... +2026-03-20 15:23:57,099 - backend.main - INFO - Grievance service initialization skipped for local dev. +2026-03-20 15:23:57,099 - backend.main - INFO - Scheduler skipped for local development +INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:10002 (Press CTRL+C to quit) +2026-03-20 15:23:57,101 - backend.main - INFO - AI services initialized successfully. +2026-03-20 15:23:57,103 - backend.main - INFO - Maharashtra data pre-loaded successfully. +2026-03-20 15:23:57,103 - backend.main - INFO - Telegram bot initialization skipped for local testing. +Starting server on port 10002 +🤖 AI Service Type: GEMINI +INFO: 127.0.0.1:46156 - "GET /health HTTP/1.1" 200 OK +INFO: Shutting down +INFO: Waiting for application shutdown. +2026-03-20 15:24:02,226 - backend.main - INFO - Shared HTTP Client closed. +2026-03-20 15:24:02,227 - root - INFO - Bot thread is not initialized +2026-03-20 15:24:02,227 - backend.main - INFO - Telegram bot thread stopped. +INFO: Application shutdown complete. +INFO: Finished server process [16397] diff --git a/server_log_fixed.txt b/server_log_fixed.txt new file mode 100644 index 0000000..50833d7 --- /dev/null +++ b/server_log_fixed.txt @@ -0,0 +1,12 @@ +2026-03-20 14:53:58,538 - backend.adaptive_weights - INFO - Adaptive weights loaded/reloaded. +2026-03-20 14:53:58,700 - backend.rag_service - INFO - Loaded 5 civic policies for RAG. +/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/pydub/utils.py:170: RuntimeWarning: Couldn't find ffmpeg or avconv - defaulting to ffmpeg, but may not work + warn("Couldn't find ffmpeg or avconv - defaulting to ffmpeg, but may not work", RuntimeWarning) +INFO: Started server process [10273] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:10000 (Press CTRL+C to quit) +INFO: Shutting down +INFO: Waiting for application shutdown. +INFO: Application shutdown complete. +INFO: Finished server process [10273] diff --git a/server_log_init.txt b/server_log_init.txt new file mode 100644 index 0000000..5afe87e --- /dev/null +++ b/server_log_init.txt @@ -0,0 +1,14 @@ +2026-03-20 15:13:18,197 - backend.adaptive_weights - INFO - Adaptive weights loaded/reloaded. +2026-03-20 15:13:18,264 - backend.rag_service - INFO - Loaded 5 civic policies for RAG. +/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/pydub/utils.py:170: RuntimeWarning: Couldn't find ffmpeg or avconv - defaulting to ffmpeg, but may not work + warn("Couldn't find ffmpeg or avconv - defaulting to ffmpeg, but may not work", RuntimeWarning) +INFO: Started server process [13485] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:10001 (Press CTRL+C to quit) +Starting server on port 10001 +INFO: 127.0.0.1:46444 - "GET /health HTTP/1.1" 200 OK +INFO: Shutting down +INFO: Waiting for application shutdown. +INFO: Application shutdown complete. +INFO: Finished server process [13485] diff --git a/start-backend.py b/start-backend.py index 6554fe8..73440cf 100644 --- a/start-backend.py +++ b/start-backend.py @@ -1,5 +1,15 @@ import os import sys + +# Fix for googletrans compatibility with newer httpcore (Issue #290) +# This monkeypatch must happen before any imports of googletrans or httpx +try: + import httpcore + if not hasattr(httpcore, "SyncHTTPTransport"): + httpcore.SyncHTTPTransport = object +except ImportError: + pass + import uvicorn # Ensure the backend directory is in the Python path