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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions app_output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/apscheduler/__init__.py:1: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Do not commit runtime log output files; remove this generated artifact from the PR and ignore it in VCS.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app_output.txt, line 1:

<comment>Do not commit runtime log output files; remove this generated artifact from the PR and ignore it in VCS.</comment>

<file context>
@@ -0,0 +1,38 @@
+/home/jules/.pyenv/versions/3.12.13/lib/python3.12/site-packages/apscheduler/__init__.py:1: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
+  from pkg_resources import get_distribution, DistributionNotFound
+2026-03-26 14:40:56,010 - backend.adaptive_weights - INFO - Adaptive weights loaded/reloaded.
</file context>
Fix with Cubic

from pkg_resources import get_distribution, DistributionNotFound
2026-03-26 15:17:56,481 - backend.adaptive_weights - INFO - Adaptive weights loaded/reloaded.
2026-03-26 15:17:56,515 - 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 [16333]
INFO: Waiting for application startup.
2026-03-26 15:17:56,898 - backend.main - INFO - Shared HTTP Client initialized.
2026-03-26 15:17:56,898 - backend.main - INFO - Starting database initialization...
2026-03-26 15:17:56,910 - backend.main - INFO - Base.metadata.create_all completed.
2026-03-26 15:17:56,910 - backend.main - INFO - Database initialized successfully (migrations skipped for local dev).
2026-03-26 15:17:56,910 - backend.main - INFO - Initializing grievance service...
2026-03-26 15:17:56,910 - backend.main - INFO - Grievance service initialization skipped for local dev.
2026-03-26 15:17:56,911 - backend.main - INFO - Scheduler skipped for local development
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
2026-03-26 15:17:56,912 - backend.main - INFO - AI services initialized successfully.
2026-03-26 15:17:56,913 - backend.main - INFO - Maharashtra data pre-loaded successfully.
2026-03-26 15:17:56,914 - backend.main - INFO - Telegram bot initialization skipped for local testing.
Starting server on port 8000
🤖 AI Service Type: GEMINI
INFO: 127.0.0.1:41058 - "GET /health HTTP/1.1" 200 OK
INFO: Shutting down
INFO: Waiting for application shutdown.
2026-03-26 15:18:11,570 - backend.main - INFO - Shared HTTP Client closed.
2026-03-26 15:18:11,570 - root - INFO - Bot thread is not initialized
2026-03-26 15:18:11,570 - backend.main - INFO - Telegram bot thread stopped.
INFO: Application shutdown complete.
INFO: Finished server process [16333]
1 change: 1 addition & 0 deletions backend/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,5 @@ def invalidate(self):
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)
visit_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=2) # For O(1) visit chaining
user_issues_cache = ThreadSafeCache(ttl=300, max_size=50) # 5 minutes TTL
21 changes: 19 additions & 2 deletions backend/geofencing_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,38 @@ def generate_visit_hash(visit_data: dict) -> str:
HMAC-SHA256 hash of visit data
"""
try:
# Normalize check_in_time to ISO format string for determinism
# Normalize check_in_time for determinism
check_in_time = visit_data.get('check_in_time')
if isinstance(check_in_time, datetime):
check_in_time_str = check_in_time.isoformat()
# Normalize to UTC and format consistently without timezone string
# This ensures consistency even if DB strips timezone info
if check_in_time.tzinfo:
check_in_time = check_in_time.astimezone(timezone.utc).replace(tzinfo=None)
check_in_time_str = check_in_time.strftime('%Y-%m-%dT%H:%M:%S')
elif isinstance(check_in_time, str):
# Try to parse and re-format for normalization
try:
# Handle ISO format with Z or +00:00
ts = check_in_time.replace('Z', '+00:00')
dt = datetime.fromisoformat(ts)
if dt.tzinfo:
dt = dt.astimezone(timezone.utc).replace(tzinfo=None)
check_in_time_str = dt.strftime('%Y-%m-%dT%H:%M:%S')
except Exception:
Comment on lines +110 to +124
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate_visit_hash now normalizes timestamps by truncating to seconds and stripping tzinfo. This changes the hashing canonicalization vs the previous isoformat() behavior and can invalidate hashes already stored in production (and also increases collision risk if two visits share the same second and other fields, especially if a chaining race causes the same previous_visit_hash). Prefer a canonical UTC ISO8601 format that preserves sub-second precision (and is consistent for both datetime and str inputs) rather than dropping microseconds/timezone information.

Suggested change
# Normalize to UTC and format consistently without timezone string
# This ensures consistency even if DB strips timezone info
if check_in_time.tzinfo:
check_in_time = check_in_time.astimezone(timezone.utc).replace(tzinfo=None)
check_in_time_str = check_in_time.strftime('%Y-%m-%dT%H:%M:%S')
elif isinstance(check_in_time, str):
# Try to parse and re-format for normalization
try:
# Handle ISO format with Z or +00:00
ts = check_in_time.replace('Z', '+00:00')
dt = datetime.fromisoformat(ts)
if dt.tzinfo:
dt = dt.astimezone(timezone.utc).replace(tzinfo=None)
check_in_time_str = dt.strftime('%Y-%m-%dT%H:%M:%S')
except Exception:
# Normalize to a canonical UTC ISO8601 representation with microseconds.
# If naive, treat as UTC; if aware, convert to UTC.
if check_in_time.tzinfo is None:
dt_utc = check_in_time.replace(tzinfo=timezone.utc)
else:
dt_utc = check_in_time.astimezone(timezone.utc)
check_in_time_str = dt_utc.isoformat(timespec="microseconds").replace("+00:00", "Z")
elif isinstance(check_in_time, str):
# Try to parse and re-format for normalization using the same UTC ISO8601 format.
try:
# Handle ISO format with Z or +00:00
ts = check_in_time.replace("Z", "+00:00")
dt = datetime.fromisoformat(ts)
if dt.tzinfo is None:
dt_utc = dt.replace(tzinfo=timezone.utc)
else:
dt_utc = dt.astimezone(timezone.utc)
check_in_time_str = dt_utc.isoformat(timespec="microseconds").replace("+00:00", "Z")
except Exception:
# Fall back to the original string if parsing fails to avoid data loss.

Copilot uses AI. Check for mistakes.
check_in_time_str = check_in_time
else:
check_in_time_str = str(check_in_time) if check_in_time else ""

# Create a deterministic string from visit data
# Chaining: include previous_visit_hash in the data string
data_string = (
f"{visit_data.get('issue_id')}"
f"{visit_data.get('officer_email')}"
f"{visit_data.get('check_in_latitude')}"
f"{visit_data.get('check_in_longitude')}"
f"{check_in_time_str}"
f"{visit_data.get('visit_notes', '')}"
f"{visit_data.get('previous_visit_hash', '')}"
)

# Generate HMAC-SHA256 hash for tamper-resistance
Expand Down
10 changes: 9 additions & 1 deletion backend/init_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,13 @@ def index_exists(table, index_name):
Base.metadata.tables['field_officer_visits'].create(bind=conn)
logger.info("Created field_officer_visits table")

# Indexes for field_officer_visits (run regardless of table creation)
# Field Officer Visits Migrations
if inspector.has_table("field_officer_visits"):
if not column_exists("field_officer_visits", "previous_visit_hash"):
conn.execute(text("ALTER TABLE field_officer_visits ADD COLUMN previous_visit_hash VARCHAR"))
logger.info("Added previous_visit_hash column to field_officer_visits")

# Indexes for field_officer_visits (run regardless of table creation)
if not index_exists("field_officer_visits", "ix_field_officer_visits_issue_id"):
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_field_officer_visits_issue_id ON field_officer_visits (issue_id)"))

Expand All @@ -199,6 +204,9 @@ def index_exists(table, index_name):
if not index_exists("field_officer_visits", "ix_field_officer_visits_check_in_time"):
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_field_officer_visits_check_in_time ON field_officer_visits (check_in_time)"))

if not index_exists("field_officer_visits", "ix_field_officer_visits_previous_visit_hash"):
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_field_officer_visits_previous_visit_hash ON field_officer_visits (previous_visit_hash)"))

logger.info("Database migration check completed successfully.")

except Exception as e:
Expand Down
6 changes: 3 additions & 3 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ 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).")
# Automated migrations for new columns (e.g. blockchain fields)
await run_in_threadpool(migrate_db)
logger.info("Database initialized successfully with migrations.")
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)
Expand Down
1 change: 1 addition & 0 deletions backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ class FieldOfficerVisit(Base):

# Immutability hash (blockchain-like integrity)
visit_hash = Column(String, nullable=True) # Hash of visit data for integrity verification
previous_visit_hash = Column(String, nullable=True, index=True) # Linked hash for O(1) verification

# Metadata
created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc))
Expand Down
50 changes: 27 additions & 23 deletions backend/requirements-render.txt
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
fastapi
uvicorn
python-dotenv
sqlalchemy
python-telegram-bot
google-generativeai
python-multipart
psycopg2-binary
huggingface-hub
httpx
pywebpush
Pillow
firebase-functions
firebase-admin
a2wsgi
python-jose[cryptography]
passlib[bcrypt]
bcrypt<4.0.0
SpeechRecognition
pydub
fastapi==0.111.0
uvicorn==0.42.0
python-dotenv==1.2.2
sqlalchemy==2.0.48
python-telegram-bot==22.7
google-generativeai==0.8.6
python-multipart==0.0.22
psycopg2-binary==2.9.11
huggingface-hub==0.36.2
httpx>=0.27.2,<0.29.0
pywebpush==2.3.0
Pillow==12.1.1
firebase-functions==0.5.0
firebase-admin==6.8.0
a2wsgi==1.10.10
python-jose[cryptography]==3.5.0
passlib[bcrypt]==1.7.4
bcrypt==3.2.2
SpeechRecognition==3.15.2
pydub==0.25.1
googletrans==4.0.2
langdetect
numpy
scikit-learn
langdetect==1.0.9
numpy==2.4.3
scikit-learn==1.8.0
python-magic==0.4.27
joblib==1.5.3
pytest==9.0.2
urllib3<2.0.0
87 changes: 85 additions & 2 deletions backend/routers/field_officer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,19 @@
OfficerCheckOutRequest,
FieldOfficerVisitResponse,
PublicFieldOfficerVisitResponse,
BlockchainVerificationResponse,
VisitHistoryResponse,
VisitStatsResponse,
VisitImageUploadResponse
)
from backend.geofencing_service import (
is_within_geofence,
generate_visit_hash,
verify_visit_integrity,
calculate_visit_metrics,
get_geofencing_service
)
from backend.cache import visit_last_hash_cache

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -92,6 +95,15 @@ def officer_check_in(request: OfficerCheckInRequest, db: Session = Depends(get_d
radius_meters=request.geofence_radius_meters or 100.0
)

# Blockchain feature: calculate integrity hash for the visit
# Performance Boost: Use thread-safe cache to eliminate DB query for last hash
prev_hash = visit_last_hash_cache.get("last_hash")
if prev_hash is None:
# Cache miss: Fetch only the last hash from DB
prev_visit = db.query(FieldOfficerVisit.visit_hash).order_by(FieldOfficerVisit.id.desc()).first()
prev_hash = prev_visit[0] if prev_visit and prev_visit[0] else ""
visit_last_hash_cache.set(data=prev_hash, key="last_hash")

Comment on lines +98 to +106
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The chaining logic is not atomic: two concurrent check-ins can read the same cached/DB prev_hash, compute different visit_hash values that both point to the same previous_visit_hash, and commit out of order, forking/breaking the intended linear chain. If this chain is meant to be tamper-evident, you need an atomic “get last hash + insert new visit + update last hash” step (e.g., a DB-backed chain state row updated in the same transaction, or a process-wide lock around this section as a minimum).

Copilot uses AI. Check for mistakes.
# Create visit record
check_in_time = datetime.now(timezone.utc)

Expand All @@ -101,10 +113,11 @@ def officer_check_in(request: OfficerCheckInRequest, db: Session = Depends(get_d
'check_in_latitude': request.check_in_latitude,
'check_in_longitude': request.check_in_longitude,
'check_in_time': check_in_time.isoformat(),
'visit_notes': request.visit_notes or ''
'visit_notes': request.visit_notes or '',
'previous_visit_hash': prev_hash
}

# Generate immutable hash
# Generate immutable hash with chaining
visit_hash = generate_visit_hash(visit_data)

new_visit = FieldOfficerVisit(
Expand All @@ -123,13 +136,17 @@ def officer_check_in(request: OfficerCheckInRequest, db: Session = Depends(get_d
visit_notes=request.visit_notes,
status='checked_in',
visit_hash=visit_hash,
previous_visit_hash=prev_hash,
is_public=True
)

db.add(new_visit)
db.commit()
db.refresh(new_visit)

# Update cache after successful commit
visit_last_hash_cache.set(data=visit_hash, key="last_hash")

logger.info(
f"Officer {request.officer_name} checked in at issue {request.issue_id}. "
f"Distance: {distance:.2f}m, Within fence: {within_fence}"
Expand All @@ -155,6 +172,8 @@ def officer_check_in(request: OfficerCheckInRequest, db: Session = Depends(get_d
status=new_visit.status,
verified_by=new_visit.verified_by,
verified_at=new_visit.verified_at,
visit_hash=new_visit.visit_hash,
previous_visit_hash=new_visit.previous_visit_hash,
is_public=new_visit.is_public,
created_at=new_visit.created_at
)
Expand Down Expand Up @@ -230,6 +249,8 @@ def officer_check_out(request: OfficerCheckOutRequest, db: Session = Depends(get
status=visit.status,
verified_by=visit.verified_by,
verified_at=visit.verified_at,
visit_hash=visit.visit_hash,
previous_visit_hash=visit.previous_visit_hash,
is_public=visit.is_public,
created_at=visit.created_at
)
Expand Down Expand Up @@ -484,3 +505,65 @@ def verify_visit(
except Exception as e:
logger.error(f"Error verifying visit {visit_id}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Verification failed")


@router.get("/field-officer/{visit_id}/blockchain-verify", response_model=BlockchainVerificationResponse)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new endpoint path is inconsistent with the existing visit-scoped routes (e.g., /field-officer/visit/{visit_id}/verify, /field-officer/visit/{visit_id}/upload-images). Consider changing it to /field-officer/visit/{visit_id}/blockchain-verify to keep the API surface predictable and avoid future route ambiguity.

Suggested change
@router.get("/field-officer/{visit_id}/blockchain-verify", response_model=BlockchainVerificationResponse)
@router.get("/field-officer/visit/{visit_id}/blockchain-verify", response_model=BlockchainVerificationResponse)

Copilot uses AI. Check for mistakes.
def verify_visit_blockchain(visit_id: int, db: Session = Depends(get_db)):
Comment on lines +510 to +511
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BlockchainVerificationResponse’s field descriptions in backend/schemas.py are issue-specific ("issue integrity", "previous issue's hash"), but this endpoint uses it for visits. Either introduce a visit-specific response schema (preferred) or update the existing schema/docs so OpenAPI accurately describes the visit verification payload.

Copilot uses AI. Check for mistakes.
"""
Verify the cryptographic integrity of a field officer visit using blockchain-style chaining.
Optimized: Uses previous_visit_hash column for O(1) verification.
"""
try:
visit = db.query(FieldOfficerVisit).filter(FieldOfficerVisit.id == visit_id).first()

if not visit:
raise HTTPException(status_code=404, detail="Visit not found")

# Chaining logic: previous_visit_hash is already stored in the record
# Chaining data components must match generate_visit_hash
visit_data = {
'issue_id': visit.issue_id,
'officer_email': visit.officer_email,
'check_in_latitude': visit.check_in_latitude,
'check_in_longitude': visit.check_in_longitude,
'check_in_time': visit.check_in_time,
'visit_notes': visit.visit_notes or '',
'previous_visit_hash': visit.previous_visit_hash or ''
}

is_valid = verify_visit_integrity(visit_data, visit.visit_hash)

# Also verify the link to previous record's hash if it exists
if is_valid and visit.previous_visit_hash:
# For O(1) we trust the stored previous_visit_hash for the current record's seal,
# but we can also check if a record with that hash actually exists
prev_exists = db.query(FieldOfficerVisit.id).filter(
FieldOfficerVisit.visit_hash == visit.previous_visit_hash
).first()
if not prev_exists:
# This doesn't mean the CURRENT record is tampered, but the CHAIN is broken
message = "Integrity verified, but chain link refers to a missing previous record."
return BlockchainVerificationResponse(
is_valid=True,
current_hash=visit.visit_hash,
computed_hash=visit.visit_hash, # verify_visit_integrity already checked this
message=message
)
Comment on lines +545 to +551
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the prev_exists failure branch, computed_hash is set to visit.visit_hash, which makes the response misleading (it no longer reflects the computed value for the payload). Compute the hash once (e.g., computed = generate_visit_hash(visit_data)) and reuse it for both is_valid and the response so clients can see what was actually recomputed.

Copilot uses AI. Check for mistakes.

if is_valid:
message = "Integrity verified. This visit record is cryptographically sealed and has not been tampered with."
else:
message = "Integrity check failed! The visit data does not match its cryptographic seal."

return BlockchainVerificationResponse(
is_valid=is_valid,
current_hash=visit.visit_hash,
computed_hash=generate_visit_hash(visit_data),
message=message
)

except HTTPException:
raise
except Exception as e:
logger.error(f"Error verifying visit {visit_id}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Verification failed")
4 changes: 4 additions & 0 deletions backend/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,8 @@ class FieldOfficerVisitResponse(BaseModel):
status: str = Field(..., description="Visit status")
verified_by: Optional[str] = Field(None, description="Verified by")
verified_at: Optional[datetime] = Field(None, description="Verification timestamp")
visit_hash: Optional[str] = Field(None, description="Integrity hash")
previous_visit_hash: Optional[str] = Field(None, description="Previous visit hash")
is_public: bool = Field(..., description="Public visibility")
created_at: datetime = Field(..., description="Creation timestamp")

Expand All @@ -541,6 +543,8 @@ class PublicFieldOfficerVisitResponse(BaseModel):
status: str = Field(..., description="Visit status")
verified_by: Optional[str] = Field(None, description="Verified by")
verified_at: Optional[datetime] = Field(None, description="Verification timestamp")
visit_hash: Optional[str] = Field(None, description="Integrity hash")
previous_visit_hash: Optional[str] = Field(None, description="Previous visit hash")
is_public: bool = Field(..., description="Public visibility")
created_at: datetime = Field(..., description="Creation timestamp")

Expand Down
Loading
Loading