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
6 changes: 5 additions & 1 deletion .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
**Learning:** Executing multiple separate `count()` queries to gather system statistics results in multiple database round-trips and redundant table scans.
**Action:** Use a single SQLAlchemy query with `func.count()` and `func.sum(case(...))` to calculate all metrics in one go. This reduces network overhead and allows the database to perform calculations in a single pass.

## 2025-02-13 - [Substring pre-filtering for regex optimization]
## 2025-02-13 - Substring pre-filtering for regex optimization
**Learning:** In hot paths (like `PriorityEngine._calculate_urgency`), executing pre-compiled regular expressions (`re.search`) for simple keyword extraction or grouping (e.g., `\b(word1|word2)\b`) is significantly slower than simple Python substring checks (`in text`). The regex engine execution overhead in Python adds up in high-iteration loops like priority scoring.
**Action:** Always consider pre-extracting literal keywords from simple regex patterns and executing a quick `any(k in text for k in keywords)` pre-filter. Only invoke `regex.search` if the pre-filter passes, avoiding the expensive regex operation on texts that obviously do not match.

## 2025-02-13 - API Route Prefix Consistency
**Learning:** Inconsistent application of `/api` prefixes between `main.py` router mounting and test suite request paths can lead to 404 errors during testing, even if the logic is correct. This is especially prevalent when multiple agents work on the same codebase with different assumptions about global prefixes.
**Action:** Always verify that `app.include_router` in `backend/main.py` uses `prefix="/api"` if the test suite (e.g., `tests/test_blockchain.py`) expects it. If a router is mounted without a prefix, ensure tests are updated or the prefix is added to `main.py` to maintain repository-wide consistency.
1 change: 1 addition & 0 deletions backend/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,5 @@ 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)
user_issues_cache = ThreadSafeCache(ttl=300, max_size=50) # 5 minutes TTL
21 changes: 20 additions & 1 deletion backend/grievance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

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
Expand All @@ -14,6 +15,7 @@
from backend.routing_service import RoutingService
from backend.sla_config_service import SLAConfigService
from backend.escalation_engine import EscalationEngine
from backend.cache import grievance_last_hash_cache

class GrievanceService:
"""
Expand Down Expand Up @@ -84,6 +86,18 @@ def create_grievance(self, grievance_data: Dict[str, Any], db: Session = None) -
# Generate unique ID
unique_id = str(uuid.uuid4())[:8].upper()

# Blockchain integrity logic
prev_hash = grievance_last_hash_cache.get("last_hash")
Copy link
Contributor

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

Choose a reason for hiding this comment

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

P1: Race condition: concurrent create_grievance calls read the same prev_hash from the cache, producing two grievances with identical previous_integrity_hash and forking the chain. The cache's internal lock only guards individual get/set calls β€” it does not make the full read β†’ hash β†’ commit β†’ update-cache sequence atomic. Wrap this section in a process-level or DB-level lock (e.g., SELECT ... FOR UPDATE on the last row, or a threading lock around the entire block) to serialize chain extension.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At backend/grievance_service.py, line 90:

<comment>Race condition: concurrent `create_grievance` calls read the same `prev_hash` from the cache, producing two grievances with identical `previous_integrity_hash` and forking the chain. The cache's internal lock only guards individual get/set calls β€” it does not make the full read β†’ hash β†’ commit β†’ update-cache sequence atomic. Wrap this section in a process-level or DB-level lock (e.g., `SELECT ... FOR UPDATE` on the last row, or a threading lock around the entire block) to serialize chain extension.</comment>

<file context>
@@ -84,6 +86,18 @@ def create_grievance(self, grievance_data: Dict[str, Any], db: Session = None) -
             unique_id = str(uuid.uuid4())[:8].upper()
 
+            # Blockchain integrity logic
+            prev_hash = grievance_last_hash_cache.get("last_hash")
+            if prev_hash is None:
+                # Fetch last hash from DB
</file context>
Fix with Cubic

if prev_hash is None:
# Fetch last hash from DB
last_grievance = db.query(Grievance.integrity_hash).order_by(Grievance.id.desc()).first()
prev_hash = last_grievance[0] if last_grievance and last_grievance[0] else ""
grievance_last_hash_cache.set(data=prev_hash, key="last_hash")
Comment on lines +90 to +95
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

grievance_last_hash_cache can return a stale last_hash when the DB has been truncated/reset (e.g., tests/test_grievance_sync.py deletes all grievances but does not clear this cache). On a cache hit, create_grievance will then chain to a hash that no longer exists in the DB, producing incorrect previous_integrity_hash and causing test/prod inconsistencies. Consider validating cache entries against the DB state (e.g., cache (last_id,last_hash) and re-fetch on mismatch), or clearing/invalidation hooks whenever grievances are bulk-deleted/reset; also consider how this behaves under concurrent grievance creations (two requests can read the same cached prev hash and fork the chain).

Suggested change
prev_hash = grievance_last_hash_cache.get("last_hash")
if prev_hash is None:
# Fetch last hash from DB
last_grievance = db.query(Grievance.integrity_hash).order_by(Grievance.id.desc()).first()
prev_hash = last_grievance[0] if last_grievance and last_grievance[0] else ""
grievance_last_hash_cache.set(data=prev_hash, key="last_hash")
# We cache both the last grievance ID and its integrity hash, and validate
# the cache against the current DB state to avoid chaining to stale hashes
cached_prev_hash = grievance_last_hash_cache.get("last_hash")
cached_last_id = grievance_last_hash_cache.get("last_id")
# Always check the actual last grievance in the DB
last_grievance = db.query(Grievance.id, Grievance.integrity_hash).order_by(Grievance.id.desc()).first()
if last_grievance:
db_last_id, db_last_hash = last_grievance
else:
db_last_id, db_last_hash = None, ""
# If cache is missing or inconsistent with DB, refresh from DB
if (
cached_prev_hash is None
or cached_last_id != db_last_id
or cached_prev_hash != db_last_hash
):
prev_hash = db_last_hash or ""
grievance_last_hash_cache.set(data=prev_hash, key="last_hash")
grievance_last_hash_cache.set(data=db_last_id, key="last_id")
else:
prev_hash = cached_prev_hash or ""

Copilot uses AI. Check for mistakes.

# Chaining: hash(unique_id|category|severity|prev_hash)
hash_content = f"{unique_id}|{grievance_data.get('category', 'general')}|{severity.value}|{prev_hash}"
integrity_hash = hashlib.sha256(hash_content.encode()).hexdigest()

Comment on lines +89 to +100
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

Serialize chain-head updates to avoid hash-chain forks under concurrency.

Line 90 (get) and Line 133 (set) are separated by non-atomic work + commit, so concurrent creates can reuse the same prev_hash and produce multiple records pointing to one predecessor.

πŸ”’ Suggested fix (transaction-level serialization)
+from sqlalchemy import text
 ...
-            # Blockchain integrity logic
+            # Blockchain integrity logic
+            # Serialize chain-head updates within the DB transaction.
+            # (Use DB-equivalent lock primitive if not on Postgres.)
+            db.execute(text("SELECT pg_advisory_xact_lock(:lock_key)"), {"lock_key": 576001})
             prev_hash = grievance_last_hash_cache.get("last_hash")
             if prev_hash is None:
                 # Fetch last hash from DB
                 last_grievance = db.query(Grievance.integrity_hash).order_by(Grievance.id.desc()).first()
                 prev_hash = last_grievance[0] if last_grievance and last_grievance[0] else ""
                 grievance_last_hash_cache.set(data=prev_hash, key="last_hash")

Also applies to: 132-133

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/grievance_service.py` around lines 89 - 100, The current flow reads
grievance_last_hash_cache.get("last_hash") and later sets it after non-atomic
work, causing concurrent creates to reuse the same prev_hash; fix by performing
the read, compute, insert, and update of the chain-head atomically inside the
same DB transaction and with a lock: open a transaction (e.g.,
db.session.begin()), acquire a row-level lock (SELECT ... FOR UPDATE) on a
dedicated chain-head record or lock the Grievance table/row used for chaining,
read the last hash (replace grievance_last_hash_cache.get usage inside the
transaction), compute integrity_hash (the existing hash_content logic), insert
the new Grievance, then update the chain-head record and call
grievance_last_hash_cache.set only after commit (or update cache inside the same
transaction), ensuring all work around grievance_last_hash_cache.get/set, the
integrity_hash calculation, and the Grievance insert occurs under that
transaction/lock to prevent forks.

# Extract location data
location_data = grievance_data.get('location', {})
latitude = location_data.get('latitude') if isinstance(location_data, dict) else None
Expand All @@ -106,13 +120,18 @@ 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()
db.refresh(grievance)

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

return grievance

except Exception as e:
Expand Down
11 changes: 11 additions & 0 deletions backend/init_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ def index_exists(table, index_name):
conn.execute(text("ALTER TABLE grievances ADD COLUMN issue_id INTEGER"))
logger.info("Added issue_id column to grievances")

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")

# Indexes
if not index_exists("grievances", "ix_grievances_latitude"):
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_grievances_latitude ON grievances (latitude)"))
Expand All @@ -160,6 +168,9 @@ def index_exists(table, index_name):
if not index_exists("grievances", "ix_grievances_issue_id"):
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_grievances_issue_id ON grievances (issue_id)"))

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)"))

if not index_exists("grievances", "ix_grievances_assigned_authority"):
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_grievances_assigned_authority ON grievances (assigned_authority)"))

Expand Down
29 changes: 14 additions & 15 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,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
Expand Down Expand Up @@ -186,18 +185,18 @@ async def lifespan(app: FastAPI):
os.makedirs("data/uploads", exist_ok=True)
app.mount("/uploads", StaticFiles(directory="data/uploads"), name="uploads")

# Include Modular Routers
app.include_router(issues.router, tags=["Issues"])
app.include_router(detection.router, tags=["Detection"])
app.include_router(grievances.router, tags=["Grievances"])
app.include_router(utility.router, tags=["Utility"])
app.include_router(auth.router, tags=["Authentication"])
app.include_router(admin.router)
app.include_router(analysis.router, tags=["Analysis"])
app.include_router(voice.router, tags=["Voice & Language"])
app.include_router(field_officer.router, tags=["Field Officer Check-In"])
app.include_router(hf.router, tags=["Hugging Face"])
app.include_router(resolution_proof.router, tags=["Resolution Proof"])
# Include Modular Routers with /api prefix
app.include_router(issues.router, prefix="/api", tags=["Issues"])
app.include_router(detection.router, prefix="/api", tags=["Detection"])
app.include_router(grievances.router, prefix="/api", tags=["Grievances"])
app.include_router(utility.router, prefix="/api", tags=["Utility"])
app.include_router(auth.router, prefix="/api", tags=["Authentication"])
app.include_router(admin.router, prefix="/api")
Copy link
Contributor

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

Choose a reason for hiding this comment

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

P2: The admin router already has prefix="/admin", so adding prefix="/api" here changes all admin endpoints from /admin/... to /api/admin/.... This breaks the documented /admin routes unless clients and docs are updated.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At backend/main.py, line 195:

<comment>The admin router already has `prefix="/admin"`, so adding `prefix="/api"` here changes all admin endpoints from `/admin/...` to `/api/admin/...`. This breaks the documented `/admin` routes unless clients and docs are updated.</comment>

<file context>
@@ -186,18 +186,18 @@ async def lifespan(app: FastAPI):
+app.include_router(grievances.router, prefix="/api", tags=["Grievances"])
+app.include_router(utility.router, prefix="/api", tags=["Utility"])
+app.include_router(auth.router, prefix="/api", tags=["Authentication"])
+app.include_router(admin.router, prefix="/api")
+app.include_router(analysis.router, prefix="/api", tags=["Analysis"])
+app.include_router(voice.router, prefix="/api", tags=["Voice & Language"])
</file context>
Suggested change
app.include_router(admin.router, prefix="/api")
app.include_router(admin.router)
Fix with Cubic

app.include_router(analysis.router, prefix="/api", tags=["Analysis"])
app.include_router(voice.router, prefix="/api", tags=["Voice & Language"])
app.include_router(field_officer.router, prefix="/api", tags=["Field Officer Check-In"])
app.include_router(hf.router, prefix="/api", tags=["Hugging Face"])
app.include_router(resolution_proof.router, prefix="/api", tags=["Resolution Proof"])

@app.get("/health")
def health():
Expand Down
4 changes: 4 additions & 0 deletions backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ class Grievance(Base):

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

# Blockchain integrity fields
integrity_hash = Column(String, nullable=True)
previous_integrity_hash = Column(String, nullable=True, index=True)

# Relationships
jurisdiction = relationship("Jurisdiction", back_populates="grievances")
audit_logs = relationship("EscalationAudit", back_populates="grievance")
Expand Down
2 changes: 0 additions & 2 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,3 @@ SpeechRecognition
pydub
googletrans==4.0.2
langdetect
indic-nlp-library
async_lru
6 changes: 3 additions & 3 deletions backend/routers/detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ async def detect_graffiti_endpoint(image: UploadFile = File(...)):
raise HTTPException(status_code=500, detail="Internal server error")


@router.post("/api/detect-traffic-sign")
@router.post("/detect-traffic-sign")
async def detect_traffic_sign_endpoint(image: UploadFile = File(...)):
# Optimized Image Processing: Validation + Optimization
_, image_bytes = await process_uploaded_image(image)
Expand All @@ -445,7 +445,7 @@ async def detect_traffic_sign_endpoint(image: UploadFile = File(...)):
raise HTTPException(status_code=500, detail="Internal server error")


@router.post("/api/detect-abandoned-vehicle")
@router.post("/detect-abandoned-vehicle")
async def detect_abandoned_vehicle_endpoint(image: UploadFile = File(...)):
# Optimized Image Processing: Validation + Optimization
_, image_bytes = await process_uploaded_image(image)
Expand All @@ -456,7 +456,7 @@ async def detect_abandoned_vehicle_endpoint(image: UploadFile = File(...)):
logger.error(f"Abandoned vehicle detection error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")

@router.post("/api/detect-emotion")
@router.post("/detect-emotion")
async def detect_emotion_endpoint(
request: Request,
image: UploadFile = File(...)
Expand Down
12 changes: 6 additions & 6 deletions backend/routers/field_officer.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
ALLOWED_IMAGE_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif', 'webp'}


@router.post("/api/field-officer/check-in", response_model=FieldOfficerVisitResponse)
@router.post("/field-officer/check-in", response_model=FieldOfficerVisitResponse)
def officer_check_in(request: OfficerCheckInRequest, db: Session = Depends(get_db)):
"""
Field officer check-in at a grievance site with GPS verification
Expand Down Expand Up @@ -166,7 +166,7 @@ def officer_check_in(request: OfficerCheckInRequest, db: Session = Depends(get_d
raise HTTPException(status_code=500, detail="Check-in failed. Please try again.")


@router.post("/api/field-officer/check-out", response_model=FieldOfficerVisitResponse)
@router.post("/field-officer/check-out", response_model=FieldOfficerVisitResponse)
def officer_check_out(request: OfficerCheckOutRequest, db: Session = Depends(get_db)):
"""
Field officer check-out from a visit
Expand Down Expand Up @@ -241,7 +241,7 @@ def officer_check_out(request: OfficerCheckOutRequest, db: Session = Depends(get
raise HTTPException(status_code=500, detail="Check-out failed. Please try again.")


@router.post("/api/field-officer/visit/{visit_id}/upload-images", response_model=VisitImageUploadResponse)
@router.post("/field-officer/visit/{visit_id}/upload-images", response_model=VisitImageUploadResponse)
async def upload_visit_images(
visit_id: int,
images: List[UploadFile] = File(..., description="Visit images"),
Expand Down Expand Up @@ -341,7 +341,7 @@ async def upload_visit_images(
raise HTTPException(status_code=500, detail="Image upload failed. Please try again.")


@router.get("/api/field-officer/issue/{issue_id}/visit-history", response_model=VisitHistoryResponse)
@router.get("/field-officer/issue/{issue_id}/visit-history", response_model=VisitHistoryResponse)
def get_issue_visit_history(
issue_id: int,
public_only: bool = True,
Expand Down Expand Up @@ -400,7 +400,7 @@ def get_issue_visit_history(
raise HTTPException(status_code=500, detail="Failed to retrieve visit history")


@router.get("/api/field-officer/visit-stats", response_model=VisitStatsResponse)
@router.get("/field-officer/visit-stats", response_model=VisitStatsResponse)
def get_visit_statistics(db: Session = Depends(get_db)):
"""
Get aggregate statistics for all field officer visits using optimized SQL queries
Expand Down Expand Up @@ -445,7 +445,7 @@ def get_visit_statistics(db: Session = Depends(get_db)):
raise HTTPException(status_code=500, detail="Failed to calculate statistics")


@router.post("/api/field-officer/visit/{visit_id}/verify")
@router.post("/field-officer/visit/{visit_id}/verify")
def verify_visit(
visit_id: int,
verifier_email: str = Form(..., description="Email of verifying admin/supervisor"),
Expand Down
53 changes: 52 additions & 1 deletion backend/routers/grievances.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
import json
import logging
import hashlib
from datetime import datetime, timezone

from backend.database import get_db
Expand All @@ -15,7 +16,8 @@
FollowGrievanceRequest, FollowGrievanceResponse,
RequestClosureRequest, RequestClosureResponse,
ConfirmClosureRequest, ConfirmClosureResponse,
ClosureStatusResponse
ClosureStatusResponse,
BlockchainVerificationResponse
)
from backend.grievance_service import GrievanceService
from backend.closure_service import ClosureService
Expand Down Expand Up @@ -436,3 +438,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=BlockchainVerificationResponse)
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.
"""
try:
grievance = db.query(
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 (O(1) from stored column)
prev_hash = grievance.previous_integrity_hash or ""

# Recompute hash based on current data and previous hash
# Chaining logic: hash(unique_id|category|severity|prev_hash)
severity_value = grievance.severity.value if hasattr(grievance.severity, 'value') else grievance.severity
hash_content = f"{grievance.unique_id}|{grievance.category}|{severity_value}|{prev_hash}"
computed_hash = hashlib.sha256(hash_content.encode()).hexdigest()
Comment on lines +464 to +471
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

This endpoint treats previous_integrity_hash=None the same as "no previous hash" by coercing it to "". For legacy grievances created before previous_integrity_hash was populated, verification will incorrectly fail even if the record hasn’t been tampered with. Consider mirroring the issues blockchain-verify behavior: if previous_integrity_hash is None, look up the previous grievance’s integrity_hash (by id < grievance_id ORDER BY id DESC) as a fallback.

Copilot uses AI. Check for mistakes.

is_valid = (computed_hash == grievance.integrity_hash)

message = "Integrity verified. This grievance record is cryptographically sealed." if is_valid \
else "Integrity check failed! The grievance data does not match its cryptographic seal."

Comment on lines +473 to +477
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

If integrity_hash is NULL (legacy data or partially-migrated rows), is_valid will be False and the message implies tampering. Consider handling integrity_hash is None explicitly with a clearer message (e.g., β€œNo integrity hash present for this grievance”), or returning a distinct status so clients can distinguish "legacy/unsealed" from "tampered".

Suggested change
is_valid = (computed_hash == grievance.integrity_hash)
message = "Integrity verified. This grievance record is cryptographically sealed." if is_valid \
else "Integrity check failed! The grievance data does not match its cryptographic seal."
if grievance.integrity_hash is None:
# Legacy or unsealed grievance: no integrity hash stored, so we cannot verify tampering.
is_valid = False
message = (
"No integrity hash present for this grievance; cryptographic integrity cannot be verified."
)
else:
is_valid = (computed_hash == grievance.integrity_hash)
message = (
"Integrity verified. This grievance record is cryptographically sealed."
if is_valid
else "Integrity check failed! The grievance data does not match its cryptographic seal."
)

Copilot uses AI. Check for mistakes.
return BlockchainVerificationResponse(
is_valid=is_valid,
current_hash=grievance.integrity_hash,
computed_hash=computed_hash,
message=message
)

except HTTPException:
raise
except Exception as e:
logger.error(f"Error verifying grievance blockchain for {grievance_id}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Failed to verify grievance integrity")
3 changes: 1 addition & 2 deletions backend/routers/issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,7 @@ async def create_issue(
# Invalidate cache so new issue appears
try:
recent_issues_cache.clear()
recent_issues_cache.clear()
user_issues_cache.clear()
user_issues_cache.clear()
except Exception as e:
logger.error(f"Error clearing cache: {e}")

Expand Down
10 changes: 5 additions & 5 deletions backend/routers/voice.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
MAX_AUDIO_SIZE = 10 * 1024 * 1024


@router.post("/api/voice/transcribe", response_model=VoiceTranscriptionResponse)
@router.post("/voice/transcribe", response_model=VoiceTranscriptionResponse)
async def transcribe_voice(
audio_file: UploadFile = File(..., description="Audio file (WAV, MP3, FLAC, etc.)"),
preferred_language: str = Form('auto', description="Preferred language code")
Expand Down Expand Up @@ -105,7 +105,7 @@ async def transcribe_voice(
raise HTTPException(status_code=500, detail=f"Voice transcription failed: {str(e)}")


@router.post("/api/voice/translate", response_model=TextTranslationResponse)
@router.post("/voice/translate", response_model=TextTranslationResponse)
def translate_text(request: TextTranslationRequest):
"""
Translate text from one language to another
Expand Down Expand Up @@ -152,7 +152,7 @@ def translate_text(request: TextTranslationRequest):
raise HTTPException(status_code=500, detail=f"Text translation failed: {str(e)}")


@router.post("/api/voice/submit-issue", response_model=IssueCreateResponse)
@router.post("/voice/submit-issue", response_model=IssueCreateResponse)
async def submit_voice_issue(
audio_file: UploadFile = File(..., description="Audio file with grievance description"),
category: str = Form(..., description="Issue category"),
Expand Down Expand Up @@ -295,7 +295,7 @@ async def submit_voice_issue(
raise HTTPException(status_code=500, detail=f"Failed to submit voice issue: {str(e)}")


@router.get("/api/voice/supported-languages", response_model=SupportedLanguagesResponse)
@router.get("/voice/supported-languages", response_model=SupportedLanguagesResponse)
def get_supported_languages():
"""
Get list of supported languages for voice transcription and translation
Expand All @@ -316,7 +316,7 @@ def get_supported_languages():
raise HTTPException(status_code=500, detail="Failed to retrieve supported languages")


@router.get("/api/voice/issue/{issue_id}/audio")
@router.get("/voice/issue/{issue_id}/audio")
def get_issue_audio(issue_id: int, db: Session = Depends(get_db)):
"""
Get the original audio file for a voice-submitted issue
Expand Down
Loading
Loading