Skip to content
7 changes: 4 additions & 3 deletions backend/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ def invalidate(self):
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
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)
blockchain_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=2)
grievance_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=5)
visit_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=5)
resolution_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=5)
user_issues_cache = ThreadSafeCache(ttl=300, max_size=50) # 5 minutes TTL
15 changes: 4 additions & 11 deletions backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,10 @@ def from_env(cls) -> "Config":
# Auth settings
secret_key = os.getenv("SECRET_KEY")
if not secret_key:
secret_key = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" # Default fallback
if environment.lower() == "production":
errors.append("SECRET_KEY is required in production environment")
else:
secret_key = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" # Fallback for dev only
# logger.warning("Using default SECRET_KEY - not safe for production")
# Only warn, don't block startup to allow health checks
pass
Comment on lines +123 to +126
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 30, 2026

Choose a reason for hiding this comment

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

P0: Critical security vulnerability: Production can now run with a hardcoded SECRET_KEY that's visible in the source code. This allows anyone to forge valid JWT authentication tokens. The previous behavior correctly blocked production startup when SECRET_KEY was missing.

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

<comment>Critical security vulnerability: Production can now run with a hardcoded `SECRET_KEY` that's visible in the source code. This allows anyone to forge valid JWT authentication tokens. The previous behavior correctly blocked production startup when `SECRET_KEY` was missing.</comment>

<file context>
@@ -120,11 +120,10 @@ def from_env(cls) -> "Config":
         # Auth settings
         secret_key = os.getenv("SECRET_KEY")
         if not secret_key:
+            secret_key = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" # Default fallback
             if environment.lower() == "production":
-                errors.append("SECRET_KEY is required in production environment")
</file context>
Suggested change
secret_key = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" # Default fallback
if environment.lower() == "production":
errors.append("SECRET_KEY is required in production environment")
else:
secret_key = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" # Fallback for dev only
# logger.warning("Using default SECRET_KEY - not safe for production")
# Only warn, don't block startup to allow health checks
pass
if environment.lower() == "production":
errors.append("SECRET_KEY is required in production environment")
else:
secret_key = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" # Fallback for dev only
Fix with Cubic

Comment on lines 121 to +126
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

Critical: Production deployments will use a publicly known secret key.

The change at lines 125-126 replaces error collection with pass, allowing production to start with the hardcoded fallback key. This key is visible in source code, meaning:

  1. Authentication bypass: Anyone can forge valid JWT tokens using the known key and algorithm (HS256).
  2. Blockchain integrity invalidated: Per context snippet 3, ResolutionProofService._sign_payload() uses this key for HMAC signing—signatures become meaningless when the key is public.

The previous behavior (adding to errors list) would block startup, forcing operators to configure a real secret. The current code defeats that safeguard.

🔒 Proposed fix: Restore production enforcement
         secret_key = os.getenv("SECRET_KEY")
         if not secret_key:
             secret_key = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" # Default fallback
             if environment.lower() == "production":
-                # Only warn, don't block startup to allow health checks
-                pass
+                errors.append("SECRET_KEY is required in production environment")

If health checks must pass before secrets are available, consider a dedicated unauthenticated health endpoint that doesn't require the full config, rather than weakening security for all production deployments.

🧰 Tools
🪛 Ruff (0.15.7)

[error] 123-123: Possible hardcoded password assigned to: "secret_key"

(S105)

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

In `@backend/config.py` around lines 121 - 126, The code currently falls back to a
hardcoded SECRET_KEY in production (variable secret_key) and silently passes,
which allows forging JWTs and breaks ResolutionProofService._sign_payload() HMAC
integrity; revert to failing startup when SECRET_KEY is missing in production by
removing the hardcoded default for production environments (or append the
missing-secret to the existing errors list and raise/exit as before), ensuring
the config loading path enforces that a real secret is provided for
environment.lower() == "production" and only allowing a non-production default
for local/dev.


algorithm = os.getenv("ALGORITHM", "HS256")
access_token_expire_minutes = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
Expand Down Expand Up @@ -207,13 +206,7 @@ class AuthConfig:

@classmethod
def from_env(cls) -> "AuthConfig":
environment = os.getenv("ENVIRONMENT", "development")
secret_key = os.getenv("SECRET_KEY")
if not secret_key:
if environment.lower() == "production":
raise ValueError("SECRET_KEY is required in production environment")
else:
secret_key = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
secret_key = os.getenv("SECRET_KEY", "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7")
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 30, 2026

Choose a reason for hiding this comment

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

P0: Critical security vulnerability: AuthConfig completely removes the production environment check, allowing production to run with the hardcoded secret key. This class is used for auth endpoints and must enforce the production requirement.

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

<comment>Critical security vulnerability: `AuthConfig` completely removes the production environment check, allowing production to run with the hardcoded secret key. This class is used for auth endpoints and must enforce the production requirement.</comment>

<file context>
@@ -207,13 +206,7 @@ class AuthConfig:
-                raise ValueError("SECRET_KEY is required in production environment")
-            else:
-                secret_key = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
+        secret_key = os.getenv("SECRET_KEY", "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7")
         algorithm = os.getenv("ALGORITHM", "HS256")
         access_token_expire_minutes = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
</file context>
Suggested change
secret_key = os.getenv("SECRET_KEY", "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7")
environment = os.getenv("ENVIRONMENT", "development")
secret_key = os.getenv("SECRET_KEY")
if not secret_key:
if environment.lower() == "production":
raise ValueError("SECRET_KEY is required in production environment")
secret_key = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
Fix with Cubic

algorithm = os.getenv("ALGORITHM", "HS256")
access_token_expire_minutes = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
return cls(
Expand Down
27 changes: 27 additions & 0 deletions backend/init_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,33 @@ def index_exists(table, index_name):
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)"))

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

if not column_exists("resolution_evidence", "previous_integrity_hash"):
conn.execute(text("ALTER TABLE resolution_evidence ADD COLUMN previous_integrity_hash VARCHAR"))
logger.info("Added previous_integrity_hash column to resolution_evidence")

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

# Resolution Proof Tokens Table Migrations
if inspector.has_table("resolution_proof_tokens"):
if not column_exists("resolution_proof_tokens", "valid_from"):
conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN valid_from TIMESTAMP"))
logger.info("Added valid_from column to resolution_proof_tokens")

if not column_exists("resolution_proof_tokens", "valid_until"):
conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN valid_until TIMESTAMP"))
logger.info("Added valid_until column to resolution_proof_tokens")

if not column_exists("resolution_proof_tokens", "nonce"):
conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN nonce VARCHAR"))
logger.info("Added nonce column to resolution_proof_tokens")
Comment on lines +223 to +234
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

Migration adds valid_from, valid_until, and nonce to resolution_proof_tokens but does not backfill existing rows. ResolutionProofService.validate_token() assumes these fields are non-NULL (uses .tzinfo and includes nonce in the signed payload), so pre-existing tokens with NULLs can crash validation or fail signature checks. Consider backfilling from generated_at/expires_at and generating a nonce, or add a defensive fallback in validation for NULL legacy rows.

Copilot uses AI. Check for mistakes.

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

except Exception as e:
Expand Down
14 changes: 6 additions & 8 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,10 @@ async def lifespan(app: FastAPI):
logger.info("Starting database initialization...")
await run_in_threadpool(Base.metadata.create_all, bind=engine)
logger.info("Base.metadata.create_all completed.")
# Temporarily disabled - comment out to debug startup issues
# await run_in_threadpool(migrate_db)
logger.info("Database initialized successfully (migrations skipped for local dev).")

# Ensure migrations run to add new blockchain columns
await run_in_threadpool(migrate_db)
logger.info("Database initialized and migrations applied successfully.")
Comment thread
RohanExploit marked this conversation as resolved.
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 Expand Up @@ -140,13 +141,10 @@ async def lifespan(app: FastAPI):

if not frontend_url:
if is_production:
raise ValueError(
"FRONTEND_URL environment variable is required for security in production. "
"Set it to your frontend URL (e.g., https://your-app.netlify.app)."
)
logger.warning("FRONTEND_URL environment variable not set in production!")
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 30, 2026

Choose a reason for hiding this comment

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

P1: Do not fall back to localhost when FRONTEND_URL is missing in production; fail fast instead. The current change silently configures production CORS with a dev origin.

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

<comment>Do not fall back to localhost when `FRONTEND_URL` is missing in production; fail fast instead. The current change silently configures production CORS with a dev origin.</comment>

<file context>
@@ -141,13 +141,10 @@ async def lifespan(app: FastAPI):
-            "FRONTEND_URL environment variable is required for security in production. "
-            "Set it to your frontend URL (e.g., https://your-app.netlify.app)."
-        )
+        logger.warning("FRONTEND_URL environment variable not set in production!")
     else:
         logger.warning("FRONTEND_URL not set. Defaulting to http://localhost:5173 for development.")
</file context>
Suggested change
logger.warning("FRONTEND_URL environment variable not set in production!")
raise ValueError(
"FRONTEND_URL environment variable is required for security in production. "
"Set it to your frontend URL (e.g., https://your-app.netlify.app)."
)
Fix with Cubic

else:
logger.warning("FRONTEND_URL not set. Defaulting to http://localhost:5173 for development.")
frontend_url = "http://localhost:5173"
frontend_url = "http://localhost:5173"

if not (frontend_url.startswith("http://") or frontend_url.startswith("https://")):
raise ValueError(
Expand Down
7 changes: 7 additions & 0 deletions backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ class ResolutionEvidence(Base):
server_signature = Column(String, nullable=True)
verification_status = Column(Enum(VerificationStatus), default=VerificationStatus.PENDING)

# Blockchain integrity fields
integrity_hash = Column(String, nullable=True)
previous_integrity_hash = Column(String, nullable=True, index=True)
Comment on lines 287 to +292
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

ResolutionEvidence currently has uploaded_at but not created_at, while the API schema (EvidenceResponse.created_at) and service/router code reference created_at. Consider adding a created_at column (or renaming/aliasing uploaded_at) and migrating the DB to keep the model and API consistent.

Copilot uses AI. Check for mistakes.

# Relationships
grievance = relationship("Grievance", back_populates="resolution_evidence")
audit_logs = relationship("EvidenceAuditLog", back_populates="evidence")
Expand All @@ -300,7 +304,10 @@ class ResolutionProofToken(Base):
token_id = Column(String, unique=True, index=True, nullable=True) # UUID string
authority_email = Column(String, nullable=True)
generated_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc))
valid_from = Column(DateTime, nullable=True)
valid_until = Column(DateTime, nullable=True)
expires_at = Column(DateTime, nullable=False)
nonce = Column(String, nullable=True)
is_used = Column(Boolean, default=False)
used_at = Column(DateTime, nullable=True)
geofence_latitude = Column(Float, nullable=True)
Expand Down
78 changes: 57 additions & 21 deletions backend/resolution_proof_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
from datetime import datetime, timedelta, timezone
from typing import Dict, Any, Optional, List, Tuple

from sqlalchemy import func
from sqlalchemy.orm import Session

from backend.models import (
Grievance, ResolutionProofToken, ResolutionEvidence,
EvidenceAuditLog, VerificationStatus, GrievanceStatus
)
from backend.config import get_config
from backend.cache import resolution_last_hash_cache

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -169,19 +171,25 @@ def generate_proof_token(
# Generate token fields
token_uuid = str(uuid.uuid4())
nonce = uuid.uuid4().hex
now = datetime.now(timezone.utc)

# Normalize timestamps (strip microseconds) for deterministic hashing/signing across databases
now = datetime.now(timezone.utc).replace(microsecond=0)
valid_until = now + timedelta(minutes=TOKEN_VALIDITY_MINUTES)

# Build signing payload
# Build signing payload using standardized ISO format without timezone offset or microseconds
# This prevents mismatches when reading back from databases like SQLite that may strip TZ
now_str = now.strftime('%Y-%m-%dT%H:%M:%S')
until_str = valid_until.strftime('%Y-%m-%dT%H:%M:%S')

payload = json.dumps({
"token_id": token_uuid,
"grievance_id": grievance_id,
"authority_email": authority_email,
"geofence_lat": grievance.latitude,
"geofence_lon": grievance.longitude,
"geofence_radius": geofence_radius,
"valid_from": now.isoformat(),
"valid_until": valid_until.isoformat(),
"valid_from": now_str,
"valid_until": until_str,
"nonce": nonce
}, sort_keys=True)

Expand All @@ -197,6 +205,7 @@ def generate_proof_token(
geofence_radius_meters=geofence_radius,
valid_from=now,
valid_until=valid_until,
expires_at=valid_until, # Maintain legacy column
nonce=nonce,
token_signature=signature,
is_used=False,
Expand Down Expand Up @@ -256,21 +265,25 @@ def validate_token(token_id: str, db: Session) -> ResolutionProofToken:
f"Valid until: {valid_until.isoformat()}, current: {now.isoformat()}"
)

# Verify signature
valid_from = token.valid_from
# Verify signature using normalized string formatting
# Ensure we handle possible None values for legacy tokens
valid_from = token.valid_from or token.generated_at
if valid_from.tzinfo is None:
valid_from = valid_from.replace(tzinfo=timezone.utc)

now_str = valid_from.strftime('%Y-%m-%dT%H:%M:%S')
until_str = valid_until.strftime('%Y-%m-%dT%H:%M:%S')

payload = json.dumps({
"token_id": token.token_id,
"grievance_id": token.grievance_id,
"authority_email": token.authority_email,
"geofence_lat": token.geofence_latitude,
"geofence_lon": token.geofence_longitude,
"geofence_radius": token.geofence_radius_meters,
"valid_from": valid_from.isoformat(),
"valid_until": valid_until.isoformat(),
"nonce": token.nonce
"valid_from": now_str,
"valid_until": until_str,
"nonce": token.nonce or ""
}, sort_keys=True)

if not ResolutionProofService._verify_signature(payload, token.token_signature):
Expand Down Expand Up @@ -352,7 +365,20 @@ def submit_evidence(
f"for grievance(s): {dup_ids}. Possible fraud."
)

# 5. Create server-side signed metadata bundle
# 5. Blockchain integrity logic
# Performance Boost: Cache-First, DB-Fallback to minimize database round-trips
prev_hash = resolution_last_hash_cache.get("last_hash")
Comment thread
RohanExploit marked this conversation as resolved.
if prev_hash is None:
# Cache miss: Fetch only the last hash from DB
prev_evidence = db.query(ResolutionEvidence.integrity_hash).order_by(ResolutionEvidence.id.desc()).first()
prev_hash = prev_evidence[0] if prev_evidence and prev_evidence[0] else ""
resolution_last_hash_cache.set(data=prev_hash, key="last_hash")
Comment thread
RohanExploit marked this conversation as resolved.

# Chaining: hash(evidence_hash|token_id|prev_hash)
chain_content = f"{evidence_hash}|{token.token_id}|{prev_hash}"
integrity_hash = hashlib.sha256(chain_content.encode()).hexdigest()

# 6. Create server-side signed metadata bundle
metadata_bundle = {
"token_id": token.token_id,
"grievance_id": token.grievance_id,
Expand All @@ -363,12 +389,14 @@ def submit_evidence(
"capture_timestamp": cap_ts.isoformat(),
"device_fingerprint_hash": device_fingerprint_hash,
"geofence_distance_meters": distance,
"integrity_hash": integrity_hash,
"previous_integrity_hash": prev_hash
}

bundle_str = json.dumps(metadata_bundle, sort_keys=True)
server_signature = ResolutionProofService._sign_payload(bundle_str)

# 6. Create evidence record
# 7. Create evidence record
evidence = ResolutionEvidence(
grievance_id=token.grievance_id,
token_id=token.id,
Expand All @@ -380,18 +408,23 @@ def submit_evidence(
metadata_bundle=metadata_bundle,
server_signature=server_signature,
verification_status=VerificationStatus.VERIFIED,
integrity_hash=integrity_hash,
previous_integrity_hash=prev_hash
)

db.add(evidence)

# 7. Mark token as used
# 8. Mark token as used
token.is_used = True
token.used_at = datetime.now(timezone.utc)

db.commit()
db.refresh(evidence)

# 8. Create audit log
# Update cache for next evidence AFTER successful commit to prevent cache poisoning
resolution_last_hash_cache.set(data=integrity_hash, key="last_hash")

# 9. Create audit log
ResolutionProofService._create_audit_log(
evidence_id=evidence.id,
action="created",
Expand Down Expand Up @@ -435,11 +468,12 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]:
Returns:
Verification result dictionary
"""
evidence_records = db.query(ResolutionEvidence).filter(
# Performance Boost: Fetch only the latest record directly instead of loading all
evidence = db.query(ResolutionEvidence).filter(
ResolutionEvidence.grievance_id == grievance_id
).all()
).order_by(ResolutionEvidence.created_at.desc()).first()
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 30, 2026

Choose a reason for hiding this comment

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

P2: ResolutionEvidence has no created_at column, so this order_by will fail at runtime. Use the existing uploaded_at timestamp (or add the column) to avoid an AttributeError/invalid SQL.

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 474:

<comment>ResolutionEvidence has no `created_at` column, so this order_by will fail at runtime. Use the existing `uploaded_at` timestamp (or add the column) to avoid an AttributeError/invalid SQL.</comment>

<file context>
@@ -471,7 +471,7 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]:
         evidence = db.query(ResolutionEvidence).filter(
             ResolutionEvidence.grievance_id == grievance_id
-        ).order_by(ResolutionEvidence.uploaded_at.desc()).first()
+        ).order_by(ResolutionEvidence.created_at.desc()).first()
 
         if not evidence:
</file context>
Fix with Cubic


Comment on lines +471 to 475
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

PR description mentions running tests/test_resolution_proof.py, but there is no such test file in backend/tests/ in this repo. Please update the PR description to match the actual test suite that was run, or add the referenced tests.

Copilot uses AI. Check for mistakes.
if not evidence_records:
if not evidence:
return {
"grievance_id": grievance_id,
"is_verified": False,
Expand All @@ -452,9 +486,6 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]:
"message": "No resolution evidence found for this grievance"
}

# Use the most recent evidence
evidence = evidence_records[-1]

# Re-verify the server signature
bundle_str = json.dumps(evidence.metadata_bundle, sort_keys=True)
signature_valid = ResolutionProofService._verify_signature(
Expand Down Expand Up @@ -483,9 +514,14 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]:

status_str = evidence.verification_status.value if evidence.verification_status else "pending"

grievance = db.query(Grievance).filter(Grievance.id == grievance_id).first()
grievance = db.query(Grievance.resolved_at).filter(Grievance.id == grievance_id).first()
resolution_ts = grievance.resolved_at if grievance else None

# Get count separately to maintain response schema
evidence_count = db.query(func.count(ResolutionEvidence.id)).filter(
ResolutionEvidence.grievance_id == grievance_id
).scalar()

return {
"grievance_id": grievance_id,
"is_verified": is_verified,
Expand All @@ -494,7 +530,7 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]:
"location_match": location_match,
"evidence_integrity": signature_valid,
"evidence_hash": evidence.evidence_hash,
"evidence_count": len(evidence_records),
"evidence_count": evidence_count,
"message": (
"Resolution verified with cryptographic proof"
if is_verified
Expand Down
7 changes: 4 additions & 3 deletions backend/routers/field_officer.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,6 @@ def officer_check_in(request: OfficerCheckInRequest, db: Session = Depends(get_d
# Generate immutable hash
visit_hash = generate_visit_hash(visit_data)

# Update cache for next visit
visit_last_hash_cache.set(data=visit_hash, key="last_hash")

new_visit = FieldOfficerVisit(
issue_id=request.issue_id,
grievance_id=request.grievance_id,
Expand All @@ -146,6 +143,10 @@ def officer_check_in(request: OfficerCheckInRequest, db: Session = Depends(get_d

db.add(new_visit)
db.commit()

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

db.refresh(new_visit)

logger.info(
Expand Down
Loading
Loading