diff --git a/app/api.py b/app/api.py index d9d265e..aff2cb0 100644 --- a/app/api.py +++ b/app/api.py @@ -245,6 +245,34 @@ def post_config(payload: ConfigPayload): return {"ok": True} +@app.get("/api/stats") +def get_stats(): + records = _read_jsonl(_score_file()) + counts: dict[str, int] = {} + for r in records: + lbl = r.get("label", "") + if lbl: + counts[lbl] = counts.get(lbl, 0) + 1 + return { + "total": len(records), + "counts": counts, + "score_file_bytes": _score_file().stat().st_size if _score_file().exists() else 0, + } + + +@app.get("/api/stats/download") +def download_stats(): + from fastapi.responses import FileResponse + if not _score_file().exists(): + raise HTTPException(404, "No score file") + return FileResponse( + str(_score_file()), + filename="email_score.jsonl", + media_type="application/jsonlines", + headers={"Content-Disposition": 'attachment; filename="email_score.jsonl"'}, + ) + + # Static SPA — MUST be last (catches all unmatched paths) _DIST = _ROOT / "web" / "dist" if _DIST.exists(): diff --git a/tests/test_api.py b/tests/test_api.py index d51b208..7665c3b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -207,3 +207,47 @@ def test_get_config_round_trips(client, config_dir): data = r.json() assert data["max_per_account"] == 300 assert data["accounts"][0]["name"] == "R" + + +# ── /api/stats ─────────────────────────────────────────────────────────────── + +@pytest.fixture +def score_with_labels(tmp_path, data_dir): + """Write a score file with 3 labels for stats tests.""" + score_path = data_dir / "email_score.jsonl" + records = [ + {"id": "a", "label": "interview_scheduled"}, + {"id": "b", "label": "interview_scheduled"}, + {"id": "c", "label": "rejected"}, + ] + score_path.write_text("\n".join(json.dumps(r) for r in records) + "\n") + return records + + +def test_stats_returns_counts(client, score_with_labels): + r = client.get("/api/stats") + assert r.status_code == 200 + data = r.json() + assert data["total"] == 3 + assert data["counts"]["interview_scheduled"] == 2 + assert data["counts"]["rejected"] == 1 + + +def test_stats_empty_when_no_file(client, data_dir): + r = client.get("/api/stats") + assert r.status_code == 200 + data = r.json() + assert data["total"] == 0 + assert data["counts"] == {} + assert data["score_file_bytes"] == 0 + + +def test_stats_download_returns_file(client, score_with_labels): + r = client.get("/api/stats/download") + assert r.status_code == 200 + assert "jsonlines" in r.headers.get("content-type", "") + + +def test_stats_download_404_when_no_file(client, data_dir): + r = client.get("/api/stats/download") + assert r.status_code == 404