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
1 change: 1 addition & 0 deletions backend/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,5 +174,6 @@ 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)
visit_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=2)
grievance_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=1)
user_issues_cache = ThreadSafeCache(ttl=300, max_size=50) # 5 minutes TTL
6 changes: 5 additions & 1 deletion backend/geofencing_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,14 @@ def generate_visit_hash(visit_data: dict) -> str:
"""
try:
# Normalize check_in_time to ISO format string for determinism
# Ensure it matches how SQLite stores/retrieves it (often without TZ)
check_in_time = visit_data.get('check_in_time')
if isinstance(check_in_time, datetime):
check_in_time_str = check_in_time.isoformat()
check_in_time_str = check_in_time.strftime('%Y-%m-%dT%H:%M:%S')
else:
check_in_time_str = str(check_in_time) if check_in_time else ""
if '+' in check_in_time_str:
check_in_time_str = check_in_time_str.split('+')[0]
Comment on lines +114 to +115
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

P1: Datetime normalization is incomplete: only + timezone offsets are stripped, so -HH:MM/Z formats can generate different hashes for the same instant.

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

<comment>Datetime normalization is incomplete: only `+` timezone offsets are stripped, so `-HH:MM`/`Z` formats can generate different hashes for the same instant.</comment>

<file context>
@@ -105,11 +105,14 @@ def generate_visit_hash(visit_data: dict) -> str:
+            check_in_time_str = check_in_time.strftime('%Y-%m-%dT%H:%M:%S')
         else:
             check_in_time_str = str(check_in_time) if check_in_time else ""
+            if '+' in check_in_time_str:
+                check_in_time_str = check_in_time_str.split('+')[0]
         
</file context>
Suggested change
if '+' in check_in_time_str:
check_in_time_str = check_in_time_str.split('+')[0]
if check_in_time_str.endswith('Z'):
check_in_time_str = check_in_time_str[:-1]
else:
for tz_sep in ['+', '-']:
if tz_sep in check_in_time_str[10:]:
check_in_time_str = check_in_time_str.rsplit(tz_sep, 1)[0]
break
Fix with Cubic

Comment on lines +114 to +115
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 | 🟡 Minor

Incomplete timezone offset handling — negative UTC offsets use -, not +.

The current logic only strips positive timezone offsets (e.g., +05:30). Negative offsets like -05:00 (UTC-5) won't be stripped, causing hash mismatches for datetimes with negative timezone offsets.

🛠️ Proposed fix to handle both positive and negative offsets
             check_in_time_str = str(check_in_time) if check_in_time else ""
-            if '+' in check_in_time_str:
-                check_in_time_str = check_in_time_str.split('+')[0]
+            # Strip timezone offset (both + and - formats)
+            for sep in ('+', '-'):
+                if sep in check_in_time_str and 'T' in check_in_time_str:
+                    # Only strip if it looks like a timezone suffix after time
+                    parts = check_in_time_str.rsplit(sep, 1)
+                    if len(parts) == 2 and ':' in parts[1]:
+                        check_in_time_str = parts[0]
+                        break

Alternatively, use a more robust approach with regex or dateutil parsing.

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

In `@backend/geofencing_service.py` around lines 114 - 115, The current
timezone-stripping logic in geofencing_service.py only handles '+' offsets for
check_in_time_str, so negative offsets like '-05:00' are left intact and cause
hash mismatches; update the code that processes check_in_time_str to remove both
positive and negative timezone offsets (e.g., detect either '+' or '-' offset
and strip the trailing offset) or, even better, normalize the timestamp using a
robust parser (e.g., dateutil.parser) to a consistent timezone/naive UTC before
hashing; locate the check_in_time_str handling and replace the single '+' split
with logic or a regex that removes / normalizes any [+-]HH:MM or 'Z' suffix.

Comment on lines +111 to +115
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

check_in_time_str normalization only strips offsets when a '+' is present. ISO-8601 strings may contain a 'Z' suffix or a negative offset (e.g., '-05:00'), which would produce a different hash for the same instant depending on representation. Normalize by parsing to a datetime (or strip both '+' and '-' offsets / 'Z') so equivalent timestamps hash identically.

Suggested change
check_in_time_str = check_in_time.strftime('%Y-%m-%dT%H:%M:%S')
else:
check_in_time_str = str(check_in_time) if check_in_time else ""
if '+' in check_in_time_str:
check_in_time_str = check_in_time_str.split('+')[0]
# If timezone-aware, convert to UTC, then drop tzinfo for a stable naive representation
if check_in_time.tzinfo is not None:
check_in_time_utc = check_in_time.astimezone(timezone.utc).replace(tzinfo=None)
else:
check_in_time_utc = check_in_time
check_in_time_str = check_in_time_utc.strftime('%Y-%m-%dT%H:%M:%S')
else:
raw_time_str = str(check_in_time) if check_in_time else ""
if raw_time_str:
# Try to parse as ISO-8601, handling 'Z' and +/- offsets so equivalent instants hash identically
iso_str = raw_time_str.rstrip()
if iso_str.endswith('Z'):
iso_str = iso_str[:-1] + '+00:00'
try:
parsed_dt = datetime.fromisoformat(iso_str)
if parsed_dt.tzinfo is not None:
parsed_dt = parsed_dt.astimezone(timezone.utc).replace(tzinfo=None)
check_in_time_str = parsed_dt.strftime('%Y-%m-%dT%H:%M:%S')
except ValueError:
# Fallback to previous behavior for non-ISO strings
check_in_time_str = raw_time_str
if '+' in check_in_time_str:
check_in_time_str = check_in_time_str.split('+')[0]
elif check_in_time_str.endswith('Z'):
check_in_time_str = check_in_time_str[:-1]
else:
check_in_time_str = ""

Copilot uses AI. Check for mistakes.

# Create a deterministic string from visit data
data_string = (
Expand All @@ -119,6 +122,7 @@ def generate_visit_hash(visit_data: dict) -> str:
f"{visit_data.get('check_in_longitude')}"
Comment on lines 118 to 122
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The hash input is built by concatenating fields with no separators, which can create ambiguous strings (different field tuples producing the same concatenation) and undermine integrity guarantees. Use an unambiguous serialization (e.g., delimiter-separated with escaping, or canonical JSON with explicit field names) before HMACing.

Copilot uses AI. Check for mistakes.
f"{check_in_time_str}"
f"{visit_data.get('visit_notes', '')}"
f"{visit_data.get('previous_visit_hash', '')}"
)

# Generate HMAC-SHA256 hash for tamper-resistance
Expand Down
7 changes: 7 additions & 0 deletions backend/init_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ def index_exists(table, index_name):

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

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

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

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

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

except Exception as e:
Expand Down
6 changes: 3 additions & 3 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ async def lifespan(app: FastAPI):
logger.info("Starting database initialization...")
await run_in_threadpool(Base.metadata.create_all, bind=engine)
logger.info("Base.metadata.create_all completed.")
# Temporarily disabled - comment out to debug startup issues
# await run_in_threadpool(migrate_db)
logger.info("Database initialized successfully (migrations skipped for local dev).")
# Enable database migrations to ensure schema is up to date
await run_in_threadpool(migrate_db)
logger.info("Database initialized and migrated successfully.")
except Exception as e:
logger.error(f"Database initialization failed: {e}", exc_info=True)
# We continue to allow health checks even if DB has issues (for debugging)
Expand Down
1 change: 1 addition & 0 deletions backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ class FieldOfficerVisit(Base):

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

# Metadata
created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc))
Expand Down
1 change: 1 addition & 0 deletions backend/requirements-render.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ python-telegram-bot
google-generativeai
python-multipart
psycopg2-binary
python-magic
huggingface-hub
httpx
pywebpush
Expand Down
89 changes: 84 additions & 5 deletions backend/routers/field_officer.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@
from backend.geofencing_service import (
is_within_geofence,
generate_visit_hash,
verify_visit_integrity,
calculate_visit_metrics,
get_geofencing_service
)
from backend.cache import visit_last_hash_cache
from backend.schemas import BlockchainVerificationResponse

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -93,18 +96,29 @@ def officer_check_in(request: OfficerCheckInRequest, db: Session = Depends(get_d
)

# Create visit record
check_in_time = datetime.now(timezone.utc)
# Fix: Strip milliseconds and timezone for deterministic hashing
check_in_time = datetime.now(timezone.utc).replace(microsecond=0)

# Blockchain feature: retrieve previous hash for chaining
# Use cache for O(1) retrieval
prev_hash = visit_last_hash_cache.get("last_hash")
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 27, 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 check-ins both read the same prev_hash from the cache before either commits, forking the blockchain chain. The read → compute → commit → cache-update sequence is not atomic. Consider using a database-level lock (e.g., SELECT … FOR UPDATE on PostgreSQL) or an application-level mutex around the entire chain-append operation.

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

<comment>Race condition: concurrent check-ins both read the same `prev_hash` from the cache before either commits, forking the blockchain chain. The read → compute → commit → cache-update sequence is not atomic. Consider using a database-level lock (e.g., `SELECT … FOR UPDATE` on PostgreSQL) or an application-level mutex around the entire chain-append operation.</comment>

<file context>
@@ -93,18 +96,29 @@ def officer_check_in(request: OfficerCheckInRequest, db: Session = Depends(get_d
+
+        # Blockchain feature: retrieve previous hash for chaining
+        # Use cache for O(1) retrieval
+        prev_hash = visit_last_hash_cache.get("last_hash")
+        if prev_hash is None:
+            # Cache miss: Fetch from DB
</file context>
Fix with Cubic

if prev_hash is None:
# Cache miss: Fetch from DB
Comment on lines +102 to +106
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Using an in-memory cache for the chain head without any DB-level locking allows concurrent check-ins to read the same prev_hash and create forks (or link to a stale head), breaking the intended single linear chain. Consider serializing head updates (e.g., via a dedicated chain-head row with SELECT ... FOR UPDATE / transactional compare-and-swap) and/or using a shared cache with atomic operations if you want to keep the O(1) optimization.

Copilot uses AI. Check for mistakes.
prev_visit = db.query(FieldOfficerVisit.visit_hash).order_by(FieldOfficerVisit.id.desc()).first()
prev_hash = prev_visit[0] if prev_visit and prev_visit[0] else ""
visit_last_hash_cache.set(data=prev_hash, key="last_hash")

visit_data = {
'issue_id': request.issue_id,
'officer_email': request.officer_email,
'check_in_latitude': request.check_in_latitude,
'check_in_longitude': request.check_in_longitude,
'check_in_time': check_in_time.isoformat(),
'visit_notes': request.visit_notes or ''
'check_in_time': check_in_time, # Pass datetime object to helper
'visit_notes': request.visit_notes or '',
'previous_visit_hash': prev_hash
}

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

new_visit = FieldOfficerVisit(
Expand All @@ -126,9 +140,14 @@ def officer_check_in(request: OfficerCheckInRequest, db: Session = Depends(get_d
is_public=True
)

new_visit.previous_visit_hash = prev_hash

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

# Update cache after successful commit
visit_last_hash_cache.set(data=visit_hash, key="last_hash")
Comment on lines +149 to +150
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The cache update after commit doesn’t prevent races/staleness across concurrent requests (or other app instances), so subsequent check-ins can still chain to an outdated head. If integrity depends on a strict chain order, the head update needs to be part of an atomic, serialized operation (DB transaction/lock or shared atomic store).

Copilot uses AI. Check for mistakes.

logger.info(
f"Officer {request.officer_name} checked in at issue {request.issue_id}. "
Expand All @@ -153,6 +172,8 @@ def officer_check_in(request: OfficerCheckInRequest, db: Session = Depends(get_d
visit_images=new_visit.visit_images,
visit_duration_minutes=new_visit.visit_duration_minutes,
status=new_visit.status,
visit_hash=new_visit.visit_hash,
previous_visit_hash=new_visit.previous_visit_hash,
verified_by=new_visit.verified_by,
verified_at=new_visit.verified_at,
is_public=new_visit.is_public,
Expand Down Expand Up @@ -228,6 +249,8 @@ def officer_check_out(request: OfficerCheckOutRequest, db: Session = Depends(get
visit_images=visit.visit_images,
visit_duration_minutes=visit.visit_duration_minutes,
status=visit.status,
visit_hash=visit.visit_hash,
previous_visit_hash=visit.previous_visit_hash,
verified_by=visit.verified_by,
verified_at=visit.verified_at,
is_public=visit.is_public,
Expand Down Expand Up @@ -478,9 +501,65 @@ def verify_visit(
logger.info(f"Visit {visit_id} verified by {verifier_email}")

return {"message": "Visit verified successfully", "visit_id": visit_id}

except HTTPException:
raise
except Exception as e:
logger.error(f"Error verifying visit {visit_id}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Verification failed")


@router.get("/field-officer/{visit_id}/blockchain-verify", response_model=BlockchainVerificationResponse)
def verify_visit_blockchain_integrity(visit_id: int, db: Session = Depends(get_db)):
"""
Verify the cryptographic integrity of a visit record using O(1) single-record verification.
"""
try:
visit = db.query(
FieldOfficerVisit.issue_id,
FieldOfficerVisit.officer_email,
FieldOfficerVisit.check_in_latitude,
FieldOfficerVisit.check_in_longitude,
FieldOfficerVisit.check_in_time,
FieldOfficerVisit.visit_notes,
FieldOfficerVisit.visit_hash,
FieldOfficerVisit.previous_visit_hash
).filter(FieldOfficerVisit.id == visit_id).first()

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

Comment on lines +528 to +530
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

HTTPException raised for missing visits (e.g., 404) will be caught by the broad except Exception and converted into a 500 response. Add an explicit except HTTPException: raise (like other endpoints in this router) before the generic exception handler so client errors propagate correctly.

Copilot uses AI. Check for mistakes.
# Determine previous hash (O(1) from stored column)
prev_hash = visit.previous_visit_hash or ""

# Reconstruct visit data for hash verification
# Normalization of time must match the generation logic
visit_data = {
'issue_id': visit.issue_id,
'officer_email': visit.officer_email,
'check_in_latitude': visit.check_in_latitude,
'check_in_longitude': visit.check_in_longitude,
'check_in_time': visit.check_in_time, # Pass datetime object
'visit_notes': visit.visit_notes or '',
'previous_visit_hash': prev_hash
}

# Verify integrity using the service helper
is_valid = verify_visit_integrity(visit_data, visit.visit_hash)
computed_hash = generate_visit_hash(visit_data)
Comment on lines +546 to +548
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

verify_visit_integrity() already recomputes the hash internally; calling generate_visit_hash() again immediately after duplicates work. Compute the hash once here and derive is_valid from that to reduce overhead and keep the logic in one place.

Suggested change
# Verify integrity using the service helper
is_valid = verify_visit_integrity(visit_data, visit.visit_hash)
computed_hash = generate_visit_hash(visit_data)
# Compute hash once and derive integrity from comparison with stored hash
computed_hash = generate_visit_hash(visit_data)
is_valid = computed_hash == visit.visit_hash

Copilot uses AI. Check for mistakes.

message = (
"Integrity verified. This visit record is cryptographically sealed and part of an immutable chain."
if is_valid
else "Integrity check failed! The record data does not match its cryptographic seal."
)

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

except Exception as e:
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

P1: Missing except HTTPException: raise before the generic except Exception handler. The 404 for a missing visit will be swallowed and returned as a 500 error to the client.

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

<comment>Missing `except HTTPException: raise` before the generic `except Exception` handler. The 404 for a missing visit will be swallowed and returned as a 500 error to the client.</comment>

<file context>
@@ -478,9 +501,65 @@ def verify_visit(
+            message=message
+        )
+
+    except Exception as e:
+        logger.error(f"Error verifying visit blockchain for {visit_id}: {e}", exc_info=True)
+        raise HTTPException(status_code=500, detail="Failed to verify visit integrity")
</file context>
Suggested change
except Exception as e:
except HTTPException:
raise
except Exception as e:
Fix with Cubic

logger.error(f"Error verifying visit blockchain for {visit_id}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Failed to verify visit integrity")
Comment on lines +511 to +565
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

Missing HTTPException re-raise — 404 errors will incorrectly return 500.

The endpoint raises HTTPException(status_code=404) at line 529, but unlike other endpoints in this file (e.g., lines 183-184, 260-261), there's no except HTTPException: raise clause. The generic except Exception at line 563 will catch the 404 and convert it to a 500 error.

🐛 Proposed fix
         return BlockchainVerificationResponse(
             is_valid=is_valid,
             current_hash=visit.visit_hash,
             computed_hash=computed_hash,
             message=message
         )

+    except HTTPException:
+        raise
     except Exception as e:
         logger.error(f"Error verifying visit blockchain for {visit_id}: {e}", exc_info=True)
-        raise HTTPException(status_code=500, detail="Failed to verify visit integrity")
+        raise HTTPException(status_code=500, detail="Failed to verify visit integrity") from e
🧰 Tools
🪛 Ruff (0.15.7)

[warning] 512-512: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


[warning] 565-565: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 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 511 - 565, The
verify_visit_blockchain_integrity endpoint currently catches all exceptions and
will turn deliberate HTTPException(404) into a 500; update the error handling in
verify_visit_blockchain_integrity by adding an explicit except HTTPException:
raise (or re-raise the caught HTTPException) before the generic except Exception
block so that HTTP errors (like the "Visit not found" 404) propagate unchanged,
leaving the final except Exception to handle unexpected errors and log/raise a
500.

2 changes: 2 additions & 0 deletions backend/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,8 @@ class FieldOfficerVisitResponse(BaseModel):
visit_images: Optional[List[str]] = Field(None, description="Visit image paths")
visit_duration_minutes: Optional[int] = Field(None, description="Visit duration")
status: str = Field(..., description="Visit status")
visit_hash: Optional[str] = Field(None, description="Integrity hash")
previous_visit_hash: Optional[str] = Field(None, description="Previous visit hash")
verified_by: Optional[str] = Field(None, description="Verified by")
verified_at: Optional[datetime] = Field(None, description="Verification timestamp")
is_public: bool = Field(..., description="Public visibility")
Expand Down
Binary file added test_blockchain.db
Binary file not shown.
27 changes: 27 additions & 0 deletions test_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

import sys
import os

# Add current directory to path
sys.path.append(os.getcwd())

try:
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

P2: Move this import smoke check into a test function and raise an assertion instead of exiting the process.

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

<comment>Move this import smoke check into a test function and raise an assertion instead of exiting the process.</comment>

<file context>
@@ -0,0 +1,27 @@
+# Add current directory to path
+sys.path.append(os.getcwd())
+
+try:
+    print("Testing imports...")
+    from backend.main import app
</file context>
Fix with Cubic

print("Testing imports...")
from backend.main import app
print("Successfully imported FastAPI app.")

from backend.models import Base
print("Successfully imported models.")

from backend.database import engine
print("Successfully imported database engine.")

from backend.routers import issues, field_officer, voice
print("Successfully imported routers.")

print("All critical imports successful.")
except Exception as e:
print(f"IMPORT ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
Loading
Loading