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
4 changes: 4 additions & 0 deletions backend/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,7 @@ def invalidate(self):
audit_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=2)
closure_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=1)
user_issues_cache = ThreadSafeCache(ttl=300, max_size=50) # 5 minutes TTL
grievances_list_cache = ThreadSafeCache(ttl=60, max_size=50) # 1 minute TTL
grievance_stats_cache = ThreadSafeCache(ttl=300, max_size=10) # 5 minutes TTL
visit_stats_cache = ThreadSafeCache(ttl=300, max_size=10) # 5 minutes TTL
evidence_audit_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=1)
13 changes: 13 additions & 0 deletions backend/init_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,19 @@ def index_exists(table, index_name):
conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN valid_until DATETIME"))
logger.info("Added valid_until column to resolution_proof_tokens")

# Evidence Audit Logs Table Migrations
if inspector.has_table("evidence_audit_logs"):
if not column_exists("evidence_audit_logs", "integrity_hash"):
conn.execute(text("ALTER TABLE evidence_audit_logs ADD COLUMN integrity_hash VARCHAR"))
logger.info("Added integrity_hash column to evidence_audit_logs")

if not column_exists("evidence_audit_logs", "previous_integrity_hash"):
Comment on lines +264 to +268
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

The migration adds integrity columns but doesn’t backfill integrity_hash / previous_integrity_hash for existing evidence_audit_logs rows. If integrity verification is expected to cover historical logs, consider a backfill step (iterate in id/timestamp order and compute the chain), or explicitly document that chaining starts only from the first post-migration entry.

Copilot uses AI. Check for mistakes.
conn.execute(text("ALTER TABLE evidence_audit_logs ADD COLUMN previous_integrity_hash VARCHAR"))
logger.info("Added previous_integrity_hash column to evidence_audit_logs")

if not index_exists("evidence_audit_logs", "ix_evidence_audit_logs_previous_integrity_hash"):
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_evidence_audit_logs_previous_integrity_hash ON evidence_audit_logs (previous_integrity_hash)"))

Comment on lines +262 to +274
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Backfill or explicitly reset the evidence-audit chain here.

This migration only adds nullable columns and the index. Any existing evidence_audit_logs rows will keep NULL hashes, so the chain can only start at the first post-deploy insert and the historical audit trail remains unverifiable. If immutability is meant to cover pre-existing logs too, this needs a one-time backfill or an explicit genesis/reset strategy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/init_db.py` around lines 262 - 274, The migration currently only adds
nullable columns (integrity_hash, previous_integrity_hash) and an index
(ix_evidence_audit_logs_previous_integrity_hash) but does not populate them; add
a one-time backfill after adding the columns that walks existing
evidence_audit_logs in deterministic order (e.g., by created_at or id) and sets
integrity_hash for each row and previous_integrity_hash to the prior row's
integrity_hash (or a fixed genesis value for the first row), or alternatively
explicitly set all previous_integrity_hash to a known genesis marker and
recompute integrity_hash accordingly; implement this backfill logic in the same
migration routine that touches evidence_audit_logs so pre-existing rows become
part of the immutable chain.

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

except Exception as e:
Expand Down
4 changes: 4 additions & 0 deletions backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,5 +336,9 @@ class EvidenceAuditLog(Base):
actor_email = Column(String, nullable=True)
timestamp = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc), index=True)

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

# Relationship
evidence = relationship("ResolutionEvidence", back_populates="audit_logs")
25 changes: 23 additions & 2 deletions backend/resolution_proof_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
EvidenceAuditLog, VerificationStatus, GrievanceStatus
)
from backend.config import get_config
from backend.cache import resolution_last_hash_cache
from backend.cache import resolution_last_hash_cache, evidence_audit_last_hash_cache

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -604,16 +604,37 @@ def _create_audit_log(
actor_email: str,
db: Session
) -> EvidenceAuditLog:
"""Create an append-only audit log entry."""
"""
Create an append-only audit log entry with blockchain integrity.
Optimized: Uses evidence_audit_last_hash_cache for O(1) chaining.
"""
Comment on lines +607 to +610
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

This change introduces new blockchain-style chaining behavior for EvidenceAuditLog, but there are existing unit tests for ResolutionProofService and none appear to cover audit-log hashing/verification. Adding tests for (a) correct chaining across multiple audit log inserts and (b) tamper detection (e.g., modifying details should invalidate the computed hash once details is included in the hash input) would help prevent regressions.

Copilot uses AI. Check for mistakes.
# Blockchain feature: calculate integrity hash for the audit log
prev_hash = evidence_audit_last_hash_cache.get("last_hash")
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Choose a reason for hiding this comment

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

P1: Cache-based previous-hash lookup is not atomic across concurrent writes, so simultaneous inserts can fork the audit chain.

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

<comment>Cache-based previous-hash lookup is not atomic across concurrent writes, so simultaneous inserts can fork the audit chain.</comment>

<file context>
@@ -604,16 +604,37 @@ def _create_audit_log(
+        Optimized: Uses evidence_audit_last_hash_cache for O(1) chaining.
+        """
+        # Blockchain feature: calculate integrity hash for the audit log
+        prev_hash = evidence_audit_last_hash_cache.get("last_hash")
+        if prev_hash is None:
+            # Cache miss: Fetch only the last hash from DB
</file context>
Fix with Cubic

if prev_hash is None:
# Cache miss: Fetch only the last hash from DB
last_audit = db.query(EvidenceAuditLog.integrity_hash).order_by(EvidenceAuditLog.id.desc()).first()
prev_hash = last_audit[0] if last_audit and last_audit[0] else ""
evidence_audit_last_hash_cache.set(data=prev_hash, key="last_hash")
Comment on lines +611 to +617
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

Using an in-process cache as the source of truth for prev_hash can break the chain in common deployments: (1) multiple app workers/processes have independent caches, so they may chain against stale last_hash; (2) concurrent inserts can read the same prev_hash and create a fork (two rows with the same previous_integrity_hash). For a strict linear chain, derive prev_hash under a DB-level lock/transaction (e.g., serialize writes or lock a dedicated “chain head” row) and treat the cache only as an optimization that is validated against the DB.

Copilot uses AI. Check for mistakes.
Comment on lines +612 to +617
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Serialize audit-log chain writes.

This get -> optional DB read -> hash -> insert -> commit -> cache set sequence is not atomic. Two concurrent requests can read the same last hash and persist rows with the same previous_integrity_hash, which forks the chain and breaks append-only verification.

Use a DB-serialized read/insert path here (for example, a dedicated lock row, advisory lock, or SELECT ... FOR UPDATE around the “read last hash + insert next row” step). The cache can stay as an optimization, but it cannot be the source of concurrency control.

Also applies to: 631-636

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/resolution_proof_service.py` around lines 612 - 617, The current
get->optional DB read->insert sequence using evidence_audit_last_hash_cache and
EvidenceAuditLog can race and produce forks; change the code to perform the
“read last hash + insert next EvidenceAuditLog row + commit” inside a
database-serialized transaction (e.g., acquire a DB-level lock or use a SELECT
... FOR UPDATE on a dedicated lock/sequence row or a DB advisory lock) so that
prev_hash (and last_audit) is read under the same lock/transaction used to
insert the new row, and only update evidence_audit_last_hash_cache after the
transaction commits; ensure the transaction scope wraps reading last_audit,
computing prev_hash, inserting the new EvidenceAuditLog, and committing.


# Chaining logic: hash(evidence_id|action|actor_email|prev_hash)
hash_content = f"{evidence_id}|{action}|{actor_email}|{prev_hash}"
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Choose a reason for hiding this comment

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

P1: Audit integrity hash excludes the details payload, so core audit content can be altered without breaking the hash chain.

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

<comment>Audit integrity hash excludes the `details` payload, so core audit content can be altered without breaking the hash chain.</comment>

<file context>
@@ -604,16 +604,37 @@ def _create_audit_log(
+            evidence_audit_last_hash_cache.set(data=prev_hash, key="last_hash")
+
+        # Chaining logic: hash(evidence_id|action|actor_email|prev_hash)
+        hash_content = f"{evidence_id}|{action}|{actor_email}|{prev_hash}"
+        integrity_hash = hashlib.sha256(hash_content.encode()).hexdigest()
+
</file context>
Fix with Cubic

integrity_hash = hashlib.sha256(hash_content.encode()).hexdigest()
Comment on lines +619 to +621
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Seal the full audit payload, and use HMAC here.

The new digest only covers evidence_id|action|actor_email|prev_hash, so details and timestamp can change without invalidating the chain. It also uses raw SHA-256 even though this feature is described as HMAC-SHA256, which means anyone who can rewrite rows can recompute valid hashes after tampering.

🔐 Example direction
-        # Chaining logic: hash(evidence_id|action|actor_email|prev_hash)
-        hash_content = f"{evidence_id}|{action}|{actor_email}|{prev_hash}"
-        integrity_hash = hashlib.sha256(hash_content.encode()).hexdigest()
+        timestamp = datetime.now(timezone.utc).replace(microsecond=0)
+        payload = json.dumps(
+            {
+                "evidence_id": evidence_id,
+                "action": action,
+                "details": details,
+                "actor_email": actor_email,
+                "timestamp": timestamp.isoformat(),
+                "previous_integrity_hash": prev_hash,
+            },
+            sort_keys=True,
+            separators=(",", ":"),
+        )
+        integrity_hash = ResolutionProofService._sign_payload(payload)

         log = EvidenceAuditLog(
             evidence_id=evidence_id,
             action=action,
             details=details,
             actor_email=actor_email,
+            timestamp=timestamp,
             integrity_hash=integrity_hash,
             previous_integrity_hash=prev_hash
         )

Also applies to: 628-629

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/resolution_proof_service.py` around lines 619 - 621, The integrity
hash only covers evidence_id, action, actor_email and prev_hash and uses raw
SHA-256; change the digest to HMAC-SHA256 over the full audit payload by
including details and timestamp in the signed content and computing an HMAC with
a secret key instead of hashlib.sha256. Locate the construction of hash_content
and integrity_hash (variables named hash_content, integrity_hash and inputs
evidence_id, action, actor_email, prev_hash) and update it to build a canonical
string that includes details and timestamp, then compute the HMAC-SHA256 using a
configured secret (e.g., HMAC_KEY or service secret) via the hmac API so
tampering cannot be trivially re-signed; apply the same change to the other
instance noted around the integrity_hash usage at the later lines (628-629).


Comment on lines +619 to +622
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

The integrity hash is currently computed from only evidence_id|action|actor_email|prev_hash, which means tampering with details (and even timestamp) would not be detectable while the “blockchain integrity” still verifies. To actually provide immutability guarantees, include all fields that must be immutable (at least details and timestamp, and ideally a canonical/serialized representation of the full log payload) in the hash input.

Copilot uses AI. Check for mistakes.
log = EvidenceAuditLog(
evidence_id=evidence_id,
action=action,
details=details,
actor_email=actor_email,
integrity_hash=integrity_hash,
previous_integrity_hash=prev_hash
)
db.add(log)
db.commit()
db.refresh(log)

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

return log

@staticmethod
Expand Down
33 changes: 23 additions & 10 deletions backend/routers/field_officer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
Issue #288: Field Officer Check-In System With Location Verification
"""

from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Response
from sqlalchemy.orm import Session
from sqlalchemy import func, case
from typing import List, Optional
import logging
import os
import json
from datetime import datetime, timezone

from backend.database import get_db
Expand All @@ -31,7 +32,7 @@
calculate_visit_metrics,
get_geofencing_service
)
from backend.cache import visit_last_hash_cache
from backend.cache import visit_last_hash_cache, visit_stats_cache
from backend.schemas import BlockchainVerificationResponse

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -424,8 +425,14 @@ def get_visit_statistics(db: Session = Depends(get_db)):
Get aggregate statistics for all field officer visits using optimized SQL queries

Returns metrics like total visits, verification status, geo-fence compliance, etc.
Optimized: Uses serialization caching and a single aggregate SQL query.
"""
try:
# Check cache
cached_json = visit_stats_cache.get("default")
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Choose a reason for hiding this comment

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

P2: visit_stats_cache is populated here with a 5-minute TTL but is never invalidated when visits are created or updated (officer_check_in, officer_check_out, verify_visit). This means /field-officer/visit-stats will serve stale counts for up to 5 minutes after any mutation. Add visit_stats_cache.invalidate("default") after each visit write commits.

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

<comment>`visit_stats_cache` is populated here with a 5-minute TTL but is never invalidated when visits are created or updated (`officer_check_in`, `officer_check_out`, `verify_visit`). This means `/field-officer/visit-stats` will serve stale counts for up to 5 minutes after any mutation. Add `visit_stats_cache.invalidate("default")` after each visit write commits.</comment>

<file context>
@@ -424,8 +425,14 @@ def get_visit_statistics(db: Session = Depends(get_db)):
     """
     try:
+        # Check cache
+        cached_json = visit_stats_cache.get("default")
+        if cached_json:
+            return Response(content=cached_json, media_type="application/json")
</file context>
Fix with Cubic

if cached_json:
return Response(content=cached_json, media_type="application/json")
Comment on lines +431 to +434
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Invalidate visit_stats_cache on visit mutations.

This cache is populated here, but the write paths in this router (officer_check_in, officer_check_out, and verify_visit) do not clear it. After any of those commits, /field-officer/visit-stats can serve stale counts for up to 5 minutes.

🧹 Example invalidation points
@@ def officer_check_in(...):
         db.commit()
         db.refresh(new_visit)
+        visit_stats_cache.invalidate("default")

@@ def officer_check_out(...):
         db.commit()
         db.refresh(visit)
+        visit_stats_cache.invalidate("default")

@@ def verify_visit(...):
         db.commit()
+        visit_stats_cache.invalidate("default")

Also applies to: 468-472

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/routers/field_officer.py` around lines 431 - 434, The visit-stats
cache is read via visit_stats_cache.get("default") but never invalidated on
visit mutations; update the mutating endpoints officer_check_in,
officer_check_out, and verify_visit to clear the cached entry after a successful
commit by removing/invalidating the "default" key (e.g.,
visit_stats_cache.delete("default") or equivalent) so /field-officer/visit-stats
cannot return stale counts; place the invalidation immediately after the DB
commit/success path in each handler and ensure it also runs on the
verified-success branch in verify_visit.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Choose a reason for hiding this comment

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

P2: Returning a raw Response bypasses the declared response_model=List[GrievanceSummaryResponse]. FastAPI skips Pydantic validation/serialization entirely for Response objects, so the response_model becomes misleading documentation only—no field filtering, aliasing, or type coercion is applied. Either remove/adjust the response_model declaration, or use response_class=ORJSONResponse and return the dict/list so FastAPI can still enforce the schema contract.

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

<comment>Returning a raw `Response` bypasses the declared `response_model=List[GrievanceSummaryResponse]`. FastAPI skips Pydantic validation/serialization entirely for `Response` objects, so the `response_model` becomes misleading documentation only—no field filtering, aliasing, or type coercion is applied. Either remove/adjust the `response_model` declaration, or use `response_class=ORJSONResponse` and return the dict/list so FastAPI can still enforce the schema contract.</comment>

<file context>
@@ -424,8 +425,14 @@ def get_visit_statistics(db: Session = Depends(get_db)):
+        # Check cache
+        cached_json = visit_stats_cache.get("default")
+        if cached_json:
+            return Response(content=cached_json, media_type="application/json")
+
         # Optimized: Use a single aggregate query to fetch multiple statistics in one database roundtrip
</file context>
Fix with Cubic


Comment on lines +431 to +435
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

response_model=VisitStatsResponse is declared, but returning a raw Response bypasses response_model validation/serialization and can desync the OpenAPI contract from runtime behavior. Consider using an optimized response_class (e.g., ORJSONResponse) while still returning stats_data (or cache the dict) so FastAPI can enforce the schema.

Copilot uses AI. Check for mistakes.
# Optimized: Use a single aggregate query to fetch multiple statistics in one database roundtrip
stats = db.query(
func.count(FieldOfficerVisit.id).label('total'),
Expand All @@ -449,14 +456,20 @@ def get_visit_statistics(db: Session = Depends(get_db)):
else:
average_distance = 0.0

return VisitStatsResponse(
total_visits=total_visits,
verified_visits=verified_visits,
within_geofence_count=within_geofence_count,
outside_geofence_count=outside_geofence_count,
unique_officers=unique_officers,
average_distance_from_site=average_distance
)
stats_data = {
"total_visits": total_visits,
"verified_visits": verified_visits,
"within_geofence_count": within_geofence_count,
"outside_geofence_count": outside_geofence_count,
"unique_officers": unique_officers,
"average_distance_from_site": average_distance
}

# Cache serialized JSON to bypass Pydantic overhead on hits
json_data = json.dumps(stats_data)
visit_stats_cache.set(json_data, "default")

return Response(content=json_data, media_type="application/json")

except Exception as e:
logger.error(f"Error calculating visit statistics: {e}", exc_info=True)
Expand Down
99 changes: 61 additions & 38 deletions backend/routers/grievances.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
ClosureStatusResponse,
BlockchainVerificationResponse
)
from backend.cache import grievances_list_cache, grievance_stats_cache
from fastapi import Response
from backend.grievance_service import GrievanceService
from backend.closure_service import ClosureService

Expand All @@ -38,9 +40,15 @@ def get_grievances(
):
"""
Get list of grievances with escalation history.
Optimized: Uses selectinload for audit_logs to avoid Cartesian product and improve O(N) fetching.
Optimized: Uses serialization caching and selectinload for audit_logs.
"""
try:
# Check cache
cache_key = f"grievances_{status}_{category}_{limit}_{offset}"
cached_json = grievances_list_cache.get(cache_key)
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Choose a reason for hiding this comment

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

P2: The new grievance list/stats caches are never invalidated after grievance mutations, so clients can receive stale escalation/list data until TTL expires.

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

<comment>The new grievance list/stats caches are never invalidated after grievance mutations, so clients can receive stale escalation/list data until TTL expires.</comment>

<file context>
@@ -38,9 +40,15 @@ def get_grievances(
     try:
+        # Check cache
+        cache_key = f"grievances_{status}_{category}_{limit}_{offset}"
+        cached_json = grievances_list_cache.get(cache_key)
+        if cached_json:
+            return Response(content=cached_json, media_type="application/json")
</file context>
Fix with Cubic

if cached_json:
return Response(content=cached_json, media_type="application/json")
Comment on lines +46 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Invalidate grievance caches after grievance state changes.

These caches are filled here, but I don't see matching invalidation when grievance state changes. manual_escalate_grievance() in this same router already mutates fields returned by /grievances and counted by /escalation-stats, so both endpoints can now serve stale data until TTL expiry.

Also applies to: 98-102, 167-170, 196-200

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/routers/grievances.py` around lines 46 - 50, The grievance list and
stats caches (populated by the cache access using cache_key =
f"grievances_{status}_{category}_{limit}_{offset}" and similar keys at the
listed locations) are not invalidated when grievance state mutators run; update
each mutating function (e.g., manual_escalate_grievance, any handlers that
change grievance.status or category or escalation counts) to call the cache
invalidation routine after a successful mutation: remove/invalidate all relevant
cached keys (the grievances_* patterns and the escalation-stats cache keys used
around lines 98-102, 167-170, 196-200) so subsequent GETs return fresh data.
Ensure invalidation happens only on success and use the same cache instance
(grievances_list_cache) used to set entries.


Comment on lines +47 to +51
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

This endpoint is declared with a response_model=List[GrievanceSummaryResponse], but returning a raw fastapi.Response bypasses FastAPI/Pydantic response_model validation/serialization and can make the OpenAPI contract misleading (no filtering/coercion happens). Consider either (a) returning Python objects (list[dict] is fine) and letting FastAPI serialize via an optimized response_class (e.g., ORJSONResponse) while caching the Python payload, or (b) removing/adjusting the response_model and explicitly documenting that the route returns pre-serialized JSON.

Copilot uses AI. Check for mistakes.
query = db.query(Grievance).options(
selectinload(Grievance.audit_logs),
joinedload(Grievance.jurisdiction)
Expand All @@ -53,41 +61,45 @@ def get_grievances(

grievances = query.offset(offset).limit(limit).all()

# Convert to response format
# Convert to response format (dictionaries for faster JSON serialization)
result = []
for grievance in grievances:
escalation_history = [
EscalationAuditResponse(
id=audit.id,
grievance_id=audit.grievance_id,
previous_authority=audit.previous_authority,
new_authority=audit.new_authority,
timestamp=audit.timestamp,
reason=audit.reason.value
)
{
"id": audit.id,
"grievance_id": audit.grievance_id,
"previous_authority": audit.previous_authority,
"new_authority": audit.new_authority,
"timestamp": audit.timestamp.isoformat() if audit.timestamp else None,
"reason": audit.reason.value
}
for audit in grievance.audit_logs
]

result.append(GrievanceSummaryResponse(
id=grievance.id,
unique_id=grievance.unique_id,
category=grievance.category,
severity=grievance.severity.value,
pincode=grievance.pincode,
city=grievance.city,
district=grievance.district,
state=grievance.state,
current_jurisdiction_id=grievance.current_jurisdiction_id,
assigned_authority=grievance.assigned_authority,
sla_deadline=grievance.sla_deadline,
status=grievance.status.value,
created_at=grievance.created_at,
updated_at=grievance.updated_at,
resolved_at=grievance.resolved_at,
escalation_history=escalation_history
))

return result
result.append({
"id": grievance.id,
"unique_id": grievance.unique_id,
"category": grievance.category,
"severity": grievance.severity.value,
"pincode": grievance.pincode,
"city": grievance.city,
"district": grievance.district,
"state": grievance.state,
"current_jurisdiction_id": grievance.current_jurisdiction_id,
"assigned_authority": grievance.assigned_authority,
"sla_deadline": grievance.sla_deadline.isoformat() if grievance.sla_deadline else None,
"status": grievance.status.value,
"created_at": grievance.created_at.isoformat() if grievance.created_at else None,
"updated_at": grievance.updated_at.isoformat() if grievance.updated_at else None,
"resolved_at": grievance.resolved_at.isoformat() if grievance.resolved_at else None,
"escalation_history": escalation_history
})

# Cache serialized JSON to bypass Pydantic overhead on hits
json_data = json.dumps(result)
grievances_list_cache.set(json_data, cache_key)

return Response(content=json_data, media_type="application/json")

except Exception as e:
logger.error(f"Error getting grievances: {e}", exc_info=True)
Expand Down Expand Up @@ -149,9 +161,14 @@ def get_grievance(grievance_id: int, db: Session = Depends(get_db)):
def get_escalation_stats(db: Session = Depends(get_db)):
"""
Get escalation statistics.
Optimized: Uses a single GROUP BY query instead of 4 separate count queries.
Optimized: Uses serialization caching and a single GROUP BY query.
"""
try:
# Check cache
cached_json = grievance_stats_cache.get("default")
if cached_json:
return Response(content=cached_json, media_type="application/json")

Comment on lines +167 to +171
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

response_model=EscalationStatsResponse is declared, but returning a raw Response bypasses response_model validation/serialization (and any alias/exclude behavior). If the goal is performance, prefer setting response_class=ORJSONResponse and returning stats_data (or cache the dict) so the response_model contract is still enforced.

Copilot uses AI. Check for mistakes.
# Perform aggregation in a single query for performance
status_counts = db.query(
Grievance.status,
Expand All @@ -168,13 +185,19 @@ def get_escalation_stats(db: Session = Depends(get_db)):

escalation_rate = (escalated_grievances / total_grievances * 100) if total_grievances > 0 else 0

return EscalationStatsResponse(
total_grievances=total_grievances,
escalated_grievances=escalated_grievances,
active_grievances=active_grievances,
resolved_grievances=resolved_grievances,
escalation_rate=escalation_rate
)
stats_data = {
"total_grievances": total_grievances,
"escalated_grievances": escalated_grievances,
"active_grievances": active_grievances,
"resolved_grievances": resolved_grievances,
"escalation_rate": escalation_rate
}

# Cache serialized JSON to bypass Pydantic overhead on hits
json_data = json.dumps(stats_data)
grievance_stats_cache.set(json_data, "default")

return Response(content=json_data, media_type="application/json")

except Exception as e:
logger.error(f"Error getting escalation stats: {e}", exc_info=True)
Expand Down
Loading