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
10 changes: 3 additions & 7 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@
**Learning:** Loading full SQLAlchemy model instances for list views or spatial checks is significantly slower and more memory-intensive than selecting only required columns, especially when tables contain large JSON or Text fields.
**Action:** Use `db.query(Model.col1, Model.col2)` for read-heavy list endpoints and spatial candidate searches. Note that projected results are immutable `Row` objects, so use `db.query(Model).filter(...).update()` for atomic modifications.

## 2026-02-07 - Transaction Consolidation for Performance
**Learning:** Performing multiple `db.commit()` calls in a single endpoint handler increases latency due to multiple round-trips and disk I/O. Using `db.flush()` allows intermediate results (like atomic increments) to be available for queries in the same transaction without the cost of a full commit.
**Action:** Consolidate multiple database updates into a single transaction. Use `db.flush()` when you need to query the database for values updated via `update()` before the final commit.

## 2026-02-08 - Return Type Consistency in Utilities
**Learning:** Inconsistent return types in shared utility functions (like `process_uploaded_image`) can cause runtime crashes across multiple modules, especially when some expect tuples and others expect single values. This can lead to deployment failures that are hard to debug without full integration logs.
**Action:** Always maintain strict return type consistency for core utilities. Use type hints and verify all call sites when changing a function's signature. Ensure that performance-oriented optimizations (like returning multiple processed formats) are applied uniformly.
## 2026-02-06 - Spatial Query Optimization
**Learning:** For small distances (e.g., < 1km), the Haversine formula is computationally expensive due to multiple trigonometric calls. An equirectangular approximation (Euclidean distance on scaled lat/lon) is ~4x faster and sufficiently accurate for pre-filtering.
**Action:** Use `equirectangular_distance_squared` as a fast pre-filter to identify candidates within radius, then compute accurate Haversine distance only for those candidates. Always handle longitude wrapping at the International Date Line. Return Haversine distances to callers for accurate great-circle measurements.
1 change: 0 additions & 1 deletion backend/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,5 +154,4 @@ def invalidate(self):

# Global instances with improved configuration
recent_issues_cache = ThreadSafeCache(ttl=300, max_size=20) # 5 minutes TTL, max 20 entries
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
4 changes: 2 additions & 2 deletions backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class Grievance(Base):
closure_approved = Column(Boolean, default=False)
pending_closure = Column(Boolean, default=False, index=True)

issue_id = Column(Integer, ForeignKey("issues.id"), nullable=True, index=True)
issue_id = Column(Integer, nullable=True, index=True)
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 9, 2026

Choose a reason for hiding this comment

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

P1: Removing ForeignKey("issues.id") breaks referential integrity between Grievance and Issue tables. The database will no longer enforce that issue_id references a valid issue, potentially leading to orphaned references. This change appears unintentional as it's unrelated to the PR's spatial optimization objective.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/models.py, line 109:

<comment>Removing `ForeignKey("issues.id")` breaks referential integrity between `Grievance` and `Issue` tables. The database will no longer enforce that `issue_id` references a valid issue, potentially leading to orphaned references. This change appears unintentional as it's unrelated to the PR's spatial optimization objective.</comment>

<file context>
@@ -106,7 +106,7 @@ class Grievance(Base):
     pending_closure = Column(Boolean, default=False, index=True)
     
-    issue_id = Column(Integer, ForeignKey("issues.id"), nullable=True, index=True)
+    issue_id = Column(Integer, nullable=True, index=True)
 
     # Relationships
</file context>
Suggested change
issue_id = Column(Integer, nullable=True, index=True)
issue_id = Column(Integer, ForeignKey("issues.id"), nullable=True, index=True)
Fix with Cubic


# Relationships
jurisdiction = relationship("Jurisdiction", back_populates="grievances")
Expand Down Expand Up @@ -145,7 +145,7 @@ class Issue(Base):

id = Column(Integer, primary_key=True, index=True)
reference_id = Column(String, unique=True, index=True) # Secure reference for government updates
description = Column(Text)
description = Column(String)
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 9, 2026

Choose a reason for hiding this comment

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

P1: Changing description from Text to String is risky for civic issue descriptions which can be lengthy. Text is designed for unlimited-length content, while String without a length specifier may cause truncation on some databases. Other similar fields in this file (notes, reason) correctly use Text. This change appears unintentional.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/models.py, line 148:

<comment>Changing `description` from `Text` to `String` is risky for civic issue descriptions which can be lengthy. `Text` is designed for unlimited-length content, while `String` without a length specifier may cause truncation on some databases. Other similar fields in this file (`notes`, `reason`) correctly use `Text`. This change appears unintentional.</comment>

<file context>
@@ -145,7 +145,7 @@ class Issue(Base):
     id = Column(Integer, primary_key=True, index=True)
     reference_id = Column(String, unique=True, index=True)  # Secure reference for government updates
-    description = Column(Text)
+    description = Column(String)
     category = Column(String, index=True)
     image_path = Column(String)
</file context>
Suggested change
description = Column(String)
description = Column(Text)
Fix with Cubic

category = Column(String, index=True)
image_path = Column(String)
source = Column(String) # 'telegram', 'web', etc.
Expand Down
2 changes: 0 additions & 2 deletions backend/requirements-render.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,3 @@ firebase-admin
a2wsgi
scikit-learn
numpy
python-jose[cryptography]
passlib[bcrypt]
170 changes: 44 additions & 126 deletions backend/routers/issues.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Query, Request, BackgroundTasks, status
from fastapi.responses import JSONResponse
from fastapi.concurrency import run_in_threadpool
Expand All @@ -17,7 +16,7 @@
IssueCreateWithDeduplicationResponse, IssueCategory, NearbyIssueResponse,
DeduplicationCheckResponse, IssueSummaryResponse, VoteResponse,
IssueStatusUpdateRequest, IssueStatusUpdateResponse, PushSubscriptionRequest,
PushSubscriptionResponse, BlockchainVerificationResponse
PushSubscriptionResponse
)
from backend.utils import (
check_upload_limits, validate_uploaded_file, save_file_blocking, save_issue_db,
Expand All @@ -29,7 +28,7 @@
send_status_notification
)
from backend.spatial_utils import get_bounding_box, find_nearby_issues
from backend.cache import recent_issues_cache, nearby_issues_cache
from backend.cache import recent_issues_cache
from backend.hf_api_service import verify_resolution_vqa
from backend.dependencies import get_http_client

Expand Down Expand Up @@ -71,11 +70,10 @@ async def create_issue(
image_path = os.path.join(upload_dir, filename)

# Process image (validate, resize, strip EXIF)
# Unpack the tuple: (PIL.Image, image_bytes)
_, image_bytes = await process_uploaded_image(image)
processed_image = await process_uploaded_image(image)

# Save processed image to disk
await run_in_threadpool(save_processed_image, image_bytes, image_path)
await run_in_threadpool(save_processed_image, processed_image, image_path)
except HTTPException:
# Re-raise HTTP exceptions (from validation)
raise
Expand Down Expand Up @@ -248,31 +246,24 @@ async def create_issue(
)

@router.post("/api/issues/{issue_id}/vote", response_model=VoteResponse)
async def upvote_issue(issue_id: int, db: Session = Depends(get_db)):
"""
Upvote an issue.
Optimized: Performs atomic update without loading full model instance.
"""
# Use update() for atomic increment and to avoid full model overhead
updated_count = await run_in_threadpool(
lambda: db.query(Issue).filter(Issue.id == issue_id).update({
Issue.upvotes: func.coalesce(Issue.upvotes, 0) + 1
}, synchronize_session=False)
)

if not updated_count:
def upvote_issue(issue_id: int, db: Session = Depends(get_db)):
issue = db.query(Issue).filter(Issue.id == issue_id).first()
if not issue:
raise HTTPException(status_code=404, detail="Issue not found")

await run_in_threadpool(db.commit)
# Increment upvotes atomically
if issue.upvotes is None:
issue.upvotes = 0

# Fetch only the updated upvote count using column projection
new_upvotes = await run_in_threadpool(
lambda: db.query(Issue.upvotes).filter(Issue.id == issue_id).scalar()
)
# Use SQLAlchemy expression for atomic update
issue.upvotes = Issue.upvotes + 1
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 9, 2026

Choose a reason for hiding this comment

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

P1: Atomic increment on NULL column will remain NULL. The check if issue.upvotes is None: issue.upvotes = 0 is ineffective because it's immediately overwritten by the SQLAlchemy column expression. In SQL, NULL + 1 = NULL. Use func.coalesce(Issue.upvotes, 0) + 1 as done elsewhere in this file.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/routers/issues.py, line 259:

<comment>Atomic increment on NULL column will remain NULL. The check `if issue.upvotes is None: issue.upvotes = 0` is ineffective because it's immediately overwritten by the SQLAlchemy column expression. In SQL, `NULL + 1 = NULL`. Use `func.coalesce(Issue.upvotes, 0) + 1` as done elsewhere in this file.</comment>

<file context>
@@ -248,31 +246,24 @@ async def create_issue(
-        lambda: db.query(Issue.upvotes).filter(Issue.id == issue_id).scalar()
-    )
+    # Use SQLAlchemy expression for atomic update
+    issue.upvotes = Issue.upvotes + 1
+
+    db.commit()
</file context>
Suggested change
issue.upvotes = Issue.upvotes + 1
issue.upvotes = func.coalesce(Issue.upvotes, 0) + 1
Fix with Cubic


db.commit()
db.refresh(issue)

return VoteResponse(
id=issue_id,
upvotes=new_upvotes or 0,
id=issue.id,
upvotes=issue.upvotes,
message="Issue upvoted successfully"
)

Expand All @@ -289,12 +280,6 @@ def get_nearby_issues(
Returns issues within the specified radius, sorted by distance.
"""
try:
# Check cache first
cache_key = f"{latitude:.5f}_{longitude:.5f}_{radius}_{limit}"
cached_data = nearby_issues_cache.get(cache_key)
if cached_data:
return cached_data

# Query open issues with coordinates
# Optimization: Use bounding box to filter candidates in SQL
min_lat, max_lat, min_lon, max_lon = get_bounding_box(latitude, longitude, radius)
Expand Down Expand Up @@ -337,9 +322,6 @@ def get_nearby_issues(
for issue, distance in nearby_issues_with_distance[:limit]
]

# Update cache
nearby_issues_cache.set(nearby_responses, cache_key)

return nearby_responses

except Exception as e:
Expand All @@ -353,23 +335,15 @@ async def verify_issue_endpoint(
image: UploadFile = File(None),
db: Session = Depends(get_db)
):
"""
Verify an issue manually or via AI.
Optimized: Uses column projection for initial check and atomic updates.
"""
# Performance Boost: Fetch only necessary columns
issue_data = await run_in_threadpool(
lambda: db.query(
Issue.id, Issue.category, Issue.status, Issue.upvotes
).filter(Issue.id == issue_id).first()
)

if not issue_data:
issue = await run_in_threadpool(lambda: db.query(Issue).filter(Issue.id == issue_id).first())
if not issue:
raise HTTPException(status_code=404, detail="Issue not found")

if image:
# AI Verification Logic
# Validate uploaded file
await validate_uploaded_file(image)
# We can ignore the returned PIL image here as we need bytes for the external API

try:
image_bytes = await image.read()
Expand All @@ -378,7 +352,7 @@ async def verify_issue_endpoint(
raise HTTPException(status_code=400, detail="Invalid image file")

# Construct question
category = issue_data.category.lower() if issue_data.category else "issue"
category = issue.category.lower() if issue.category else "issue"
question = f"Is there a {category} in this image?"

# Custom questions for common categories
Expand All @@ -394,23 +368,22 @@ async def verify_issue_endpoint(
question = "Is there a fallen tree?"

try:
# Use shared client dependency is tricky here because logic is mixed
# request.app.state.http_client is available
client = request.app.state.http_client
result = await verify_resolution_vqa(image_bytes, question, client)

answer = result.get('answer', 'unknown')
confidence = result.get('confidence', 0)

# If the answer is "no" (meaning the issue is NOT present), we consider it resolved.
is_resolved = False
if answer.lower() in ["no", "none", "nothing"] and confidence > 0.5:
is_resolved = True
if issue_data.status != "resolved":
# Perform update using primary key
await run_in_threadpool(
lambda: db.query(Issue).filter(Issue.id == issue_id).update({
Issue.status: "verified",
Issue.verified_at: datetime.now(timezone.utc)
}, synchronize_session=False)
)
# Update status if not already resolved
if issue.status != "resolved":
issue.status = "verified" # Mark as verified (resolved usually implies closed)
issue.verified_at = datetime.now(timezone.utc)
await run_in_threadpool(db.commit)

return {
Expand All @@ -424,41 +397,28 @@ async def verify_issue_endpoint(
raise HTTPException(status_code=500, detail="Verification service temporarily unavailable")
else:
# Manual Verification Logic (Vote)
# Atomic increment by 2 for verification
# Optimized: Use a single transaction for all updates
await run_in_threadpool(
lambda: db.query(Issue).filter(Issue.id == issue_id).update({
Issue.upvotes: func.coalesce(Issue.upvotes, 0) + 2
}, synchronize_session=False)
)
# Increment upvotes (verification counts as strong support)
if issue.upvotes is None:
issue.upvotes = 0

# Flush to DB so we can query the updated value within the same transaction
await run_in_threadpool(db.flush)
# Atomic increment
issue.upvotes = Issue.upvotes + 2
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 9, 2026

Choose a reason for hiding this comment

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

P1: Same NULL handling bug as upvote_issue. The SQLAlchemy column expression Issue.upvotes + 2 generates SQL that will leave NULL values unchanged. Use func.coalesce(Issue.upvotes, 0) + 2 instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/routers/issues.py, line 405:

<comment>Same NULL handling bug as `upvote_issue`. The SQLAlchemy column expression `Issue.upvotes + 2` generates SQL that will leave NULL values unchanged. Use `func.coalesce(Issue.upvotes, 0) + 2` instead.</comment>

<file context>
@@ -424,41 +397,28 @@ async def verify_issue_endpoint(
-        # Flush to DB so we can query the updated value within the same transaction
-        await run_in_threadpool(db.flush)
+        # Atomic increment
+        issue.upvotes = Issue.upvotes + 2
 
-        # Performance Boost: Fetch only needed fields to check auto-verification threshold
</file context>
Suggested change
issue.upvotes = Issue.upvotes + 2
issue.upvotes = func.coalesce(Issue.upvotes, 0) + 2
Fix with Cubic


# Performance Boost: Fetch only needed fields to check auto-verification threshold
# This query is performed within the same transaction after flush
updated_issue = await run_in_threadpool(
lambda: db.query(Issue.upvotes, Issue.status).filter(Issue.id == issue_id).first()
)
# If issue has enough verifications, consider upgrading status
# Use flush to apply increment within transaction, then refresh to check value
await run_in_threadpool(db.flush)
await run_in_threadpool(db.refresh, issue)

final_status = updated_issue.status if updated_issue else "open"
final_upvotes = updated_issue.upvotes if updated_issue else 0
if issue.upvotes >= 5 and issue.status == "open":
issue.status = "verified"
logger.info(f"Issue {issue_id} automatically verified due to {issue.upvotes} upvotes")

if updated_issue and updated_issue.upvotes >= 5 and updated_issue.status == "open":
await run_in_threadpool(
lambda: db.query(Issue).filter(Issue.id == issue_id).update({
Issue.status: "verified"
}, synchronize_session=False)
)
logger.info(f"Issue {issue_id} automatically verified due to {updated_issue.upvotes} upvotes")
final_status = "verified"

# Final commit for all changes in the transaction
# Commit all changes (upvote and potential status change)
await run_in_threadpool(db.commit)

return VoteResponse(
id=issue_id,
upvotes=final_upvotes,
id=issue.id,
upvotes=issue.upvotes,
message="Issue verified successfully"
)

Expand Down Expand Up @@ -604,48 +564,6 @@ def get_user_issues(

return data

@router.get("/api/issues/{issue_id}/blockchain-verify", response_model=BlockchainVerificationResponse)
async def verify_blockchain_integrity(issue_id: int, db: Session = Depends(get_db)):
"""
Verify the cryptographic integrity of a report using the blockchain-style chaining.
Optimized: Uses column projection to fetch only needed data.
"""
# Fetch current issue data
current_issue = await run_in_threadpool(
lambda: db.query(
Issue.id, Issue.description, Issue.category, Issue.integrity_hash
).filter(Issue.id == issue_id).first()
)

if not current_issue:
raise HTTPException(status_code=404, detail="Issue not found")

# Fetch previous issue's integrity hash to verify the chain
prev_issue_hash = await run_in_threadpool(
lambda: db.query(Issue.integrity_hash).filter(Issue.id < issue_id).order_by(Issue.id.desc()).first()
)

prev_hash = prev_issue_hash[0] if prev_issue_hash and prev_issue_hash[0] else ""

# Recompute hash based on current data and previous hash
# Chaining logic: hash(description|category|prev_hash)
hash_content = f"{current_issue.description}|{current_issue.category}|{prev_hash}"
computed_hash = hashlib.sha256(hash_content.encode()).hexdigest()

is_valid = (computed_hash == current_issue.integrity_hash)

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

return BlockchainVerificationResponse(
is_valid=is_valid,
current_hash=current_issue.integrity_hash,
computed_hash=computed_hash,
message=message
)

@router.get("/api/issues/recent", response_model=List[IssueSummaryResponse])
def get_recent_issues(
limit: int = Query(10, ge=1, le=50, description="Number of issues to return"),
Expand Down
16 changes: 5 additions & 11 deletions backend/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@

class IssueCategory(str, Enum):
ROAD = "Road"
WATER = "Water"
STREETLIGHT = "Streetlight"
GARBAGE = "Garbage"
COLLEGE_INFRA = "College Infra"
WOMEN_SAFETY = "Women Safety"

class UserRole(str, Enum):
ADMIN = "admin"
USER = "user"
OFFICIAL = "official"
WATER = "Water"
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 9, 2026

Choose a reason for hiding this comment

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

P0: Critical bug: Issue categories were incorrectly moved to UserRole enum. This breaks issue creation for all categories except 'Road' since IssueCategory is now missing these values. Values like WATER, GARBAGE, STREETLIGHT are semantically issue categories, not user roles. These lines should be removed from UserRole and restored to IssueCategory.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/schemas.py, line 13:

<comment>Critical bug: Issue categories were incorrectly moved to `UserRole` enum. This breaks issue creation for all categories except 'Road' since `IssueCategory` is now missing these values. Values like `WATER`, `GARBAGE`, `STREETLIGHT` are semantically issue categories, not user roles. These lines should be removed from `UserRole` and restored to `IssueCategory`.</comment>

<file context>
@@ -5,16 +5,16 @@
     ADMIN = "admin"
     USER = "user"
     OFFICIAL = "official"
+    WATER = "Water"
+    STREETLIGHT = "Streetlight"
+    GARBAGE = "Garbage"
</file context>
Fix with Cubic

STREETLIGHT = "Streetlight"
GARBAGE = "Garbage"
COLLEGE_INFRA = "College Infra"
WOMEN_SAFETY = "Women Safety"

class IssueStatus(str, Enum):
OPEN = "open"
Expand Down Expand Up @@ -272,12 +272,6 @@ class ClosureStatusResponse(BaseModel):
confirmation_deadline: Optional[datetime] = Field(None, description="Deadline for confirmations")
days_remaining: Optional[int] = Field(None, description="Days until deadline")

class BlockchainVerificationResponse(BaseModel):
is_valid: bool = Field(..., description="Whether the issue 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 issue data and previous issue's hash")
message: str = Field(..., description="Verification result message")

# Auth Schemas
class UserBase(BaseModel):
email: str = Field(..., description="User email")
Expand Down
Loading