Skip to content
1 change: 1 addition & 0 deletions backend/app/routers/analyze.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Full analysis router - POST /analyze/ and POST /analyze/zip/."""

from __future__ import annotations

import time
Expand Down
50 changes: 40 additions & 10 deletions backend/app/routers/share.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,18 @@ def create_share(payload: ShareCreateRequest, db: Session = Depends(get_db)):
token = ""
for _ in range(5):
candidate = secrets.token_urlsafe(8)
exists = db.execute(select(SharedSnippet).where(SharedSnippet.token == candidate)).scalar_one_or_none()
exists = db.execute(
select(SharedSnippet).where(SharedSnippet.token == candidate)
).scalar_one_or_none()
if exists is None:
token = candidate
break

if not token:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Could not create share token")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Could not create share token",
)

record = SharedSnippet(
token=token,
Expand All @@ -56,13 +61,24 @@ def get_share(token: str, db: Session = Depends(get_db)):

_Base.metadata.create_all(bind=db.get_bind())

record = db.execute(select(SharedSnippet).where(SharedSnippet.token == token)).scalar_one_or_none()
record = db.execute(
select(SharedSnippet).where(SharedSnippet.token == token)
).scalar_one_or_none()
if record is None:
# fallback: try raw SQL in case ORM mapping/env differences hide the record
from sqlalchemy import text
raw = db.execute(text("SELECT token, code, result_json, created_at FROM shares WHERE token = :t"), {"t": token}).first()

raw = db.execute(
text(
"SELECT token, code, result_json, created_at FROM shares WHERE token = :t"
),
{"t": token},
).first()
if raw is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Shared result not found or expired")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Shared result not found or expired",
)

# parse created_at which may be string or datetime
token_val, code_val, result_json_val, created_at_val = raw
Expand All @@ -74,20 +90,32 @@ def get_share(token: str, db: Session = Depends(get_db)):
created_at = _dt.datetime.fromisoformat(created_at)
except Exception:
try:
created_at = _dt.datetime.strptime(created_at, "%Y-%m-%d %H:%M:%S.%f")
created_at = _dt.datetime.strptime(
created_at, "%Y-%m-%d %H:%M:%S.%f"
)
except Exception:
created_at = None

if created_at is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Shared result not found or expired")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Shared result not found or expired",
)

if created_at.tzinfo is None:
created_at = created_at.replace(tzinfo=_dt.timezone.utc)

if created_at < _dt.datetime.now(_dt.timezone.utc) - _dt.timedelta(days=7):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Shared result expired")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Shared result expired"
)

return ShareRecord(id=token_val, code=code_val, result=json.loads(result_json_val), created_at=created_at.isoformat())
return ShareRecord(
id=token_val,
code=code_val,
result=json.loads(result_json_val),
created_at=created_at.isoformat(),
)

# expire shares older than 7 days — normalize tzinfo if necessary
from datetime import datetime, timezone, timedelta
Expand All @@ -97,7 +125,9 @@ def get_share(token: str, db: Session = Depends(get_db)):
created_at = created_at.replace(tzinfo=timezone.utc)

if created_at < datetime.now(timezone.utc) - timedelta(days=7):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Shared result expired")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Shared result expired"
)

return ShareRecord(
id=record.token,
Expand Down
24 changes: 18 additions & 6 deletions backend/app/services/code_assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@

# ── Language Detection ─────────────────────────────────────────────────────────
LANG_SIGNATURES: dict[str, list[str]] = {
"Swift": [
r"\bimport\s+Foundation\b",
r"\bfunc\s+\w+\s*\(",
r"\b(let|var)\s+\w+\s*:\s*\w+",
r"\bclass\s+\w+",
r"\bstruct\s+\w+",
r"\bprotocol\s+\w+",
r"\bextension\s+\w+",
r"\bguard\s+let\b",
r"\bprint\s*\(",
],
"Python": [
r"\bdef\s+\w+\s*\(",
r"\bimport\s+\w+",
Expand Down Expand Up @@ -52,13 +63,13 @@
r"\bint\s+main\s*\(",
r"::\w+",
],
"Swift": [
"Go": [
r"\bfunc\s+\w+\s*\(",
r"\bvar\s+\w+\s*:",
r"\blet\s+\w+\s*=",
r"print\s*\(",
r"import\s+\w+",
r"guard\s+let\b",
r"\bpackage\s+\w+",
r'\bimport\s+"',
r"fmt\.Print",
r"\bgo\s+func\b",
r":=",
],
"PHP": [
r"<\?php",
Expand Down Expand Up @@ -215,6 +226,7 @@ class BugPattern:
"C++",
"PHP",
"Rust",
"Swift",
]
)

Expand Down
16 changes: 8 additions & 8 deletions backend/app/services/email_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,15 @@ def _build_html(stats: dict, unsubscribe_url: str) -> str:
score_line = f"""
<tr>
<td style="padding:8px 12px;border-bottom:1px solid #e0dcd4;"><strong>Average Score</strong></td>
<td style="padding:8px 12px;border-bottom:1px solid #e0dcd4;text-align:right;">{stats['avg_score']}/100 {emoji}{change}</td>
<td style="padding:8px 12px;border-bottom:1px solid #e0dcd4;text-align:right;">{stats["avg_score"]}/100 {emoji}{change}</td>
</tr>"""

bug_line = ""
if stats["top_bug"]:
bug_line = f"""
<tr>
<td style="padding:8px 12px;border-bottom:1px solid #e0dcd4;"><strong>Most Common Bug</strong></td>
<td style="padding:8px 12px;border-bottom:1px solid #e0dcd4;text-align:right;">{stats['top_bug']}</td>
<td style="padding:8px 12px;border-bottom:1px solid #e0dcd4;text-align:right;">{stats["top_bug"]}</td>
</tr>"""

return f"""<!DOCTYPE html>
Expand All @@ -176,32 +176,32 @@ def _build_html(stats: dict, unsubscribe_url: str) -> str:
<table role="presentation" width="560" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.08);">
<tr><td style="background:#1a1a2e;padding:24px 32px;text-align:center;">
<h1 style="margin:0;color:#f0a030;font-size:1.4rem;">QyverixAI Weekly Digest</h1>
<p style="margin:4px 0 0;color:#aaa;font-size:0.85rem;">{stats['week_start']} – {stats['week_end']}</p>
<p style="margin:4px 0 0;color:#aaa;font-size:0.85rem;">{stats["week_start"]} – {stats["week_end"]}</p>
</td></tr>
<tr><td style="padding:24px 32px;">
<p style="margin:0 0 16px;color:#333;font-size:0.95rem;">Here&#39;s your weekly code analysis summary.</p>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e0dcd4;border-radius:6px;">
<tr>
<td style="padding:8px 12px;border-bottom:1px solid #e0dcd4;"><strong>Analyses Run</strong></td>
<td style="padding:8px 12px;border-bottom:1px solid #e0dcd4;text-align:right;">{stats['total_analyses']}</td>
<td style="padding:8px 12px;border-bottom:1px solid #e0dcd4;text-align:right;">{stats["total_analyses"]}</td>
</tr>
<tr>
<td style="padding:8px 12px;border-bottom:1px solid #e0dcd4;"><strong>Languages</strong></td>
<td style="padding:8px 12px;border-bottom:1px solid #e0dcd4;text-align:right;">{', '.join(stats['languages'])}</td>
<td style="padding:8px 12px;border-bottom:1px solid #e0dcd4;text-align:right;">{", ".join(stats["languages"])}</td>
</tr>
{score_line}
<tr>
<td style="padding:8px 12px;border-bottom:1px solid #e0dcd4;"><strong>Issues Found</strong></td>
<td style="padding:8px 12px;border-bottom:1px solid #e0dcd4;text-align:right;">{stats['total_issues']}</td>
<td style="padding:8px 12px;border-bottom:1px solid #e0dcd4;text-align:right;">{stats["total_issues"]}</td>
</tr>
{bug_line}
</table>
</td></tr>
<tr><td style="padding:0 32px 24px;text-align:center;">
<a href="{stats.get('base_url', 'https://qyverixai.onrender.com')}/app" style="display:inline-block;padding:10px 24px;background:#f0a030;color:#1a1a2e;text-decoration:none;border-radius:6px;font-weight:bold;font-size:0.9rem;">Open QyverixAI</a>
<a href="{stats.get("base_url", "https://qyverixai.onrender.com")}/app" style="display:inline-block;padding:10px 24px;background:#f0a030;color:#1a1a2e;text-decoration:none;border-radius:6px;font-weight:bold;font-size:0.9rem;">Open QyverixAI</a>
</td></tr>
<tr><td style="padding:16px 32px;background:#faf8f5;font-size:0.75rem;color:#888;text-align:center;">
<p style="margin:0;">This email was sent to {stats['email']} because you subscribed to the QyverixAI weekly digest.</p>
<p style="margin:0;">This email was sent to {stats["email"]} because you subscribed to the QyverixAI weekly digest.</p>
<p style="margin:4px 0 0;"><a href="{unsubscribe_url}" style="color:#888;text-decoration:underline;">Unsubscribe</a></p>
</td></tr>
</table>
Expand Down
3 changes: 2 additions & 1 deletion backend/tests/test_digest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

import sys, os
import sys
import os

sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))

Expand Down
12 changes: 9 additions & 3 deletions backend/tests/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@
QyverixAI — Test Suite
Run: cd backend && pytest -v
"""
import io
import zipfile

import os
import sys

import pytest
from fastapi.testclient import TestClient
import sys, os
<<<<<<< HEAD
=======
import sys
import os
>>>>>>> fix/labeler-yaml

sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))

from app import main as app_main

client = TestClient(app_main.app)
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_share.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def _configure_test_db(monkeypatch, tmp_path):


def test_create_and_fetch_share(monkeypatch, tmp_path):
session_local = _configure_test_db(monkeypatch, tmp_path)
_configure_test_db(monkeypatch, tmp_path)

from fastapi.testclient import TestClient

Expand Down