feat(signals): add body/from_addr to signal query; add reclassify endpoint
This commit is contained in:
parent
e24e0b7233
commit
1d943ed8a3
2 changed files with 75 additions and 2 deletions
32
dev-api.py
32
dev-api.py
|
|
@ -306,7 +306,7 @@ def list_interviews():
|
||||||
sig_placeholders = ",".join("?" * len(job_ids))
|
sig_placeholders = ",".join("?" * len(job_ids))
|
||||||
excl_placeholders = ",".join("?" * len(SIGNAL_EXCLUDED))
|
excl_placeholders = ",".join("?" * len(SIGNAL_EXCLUDED))
|
||||||
sig_rows = db.execute(
|
sig_rows = db.execute(
|
||||||
f"SELECT id, job_id, subject, received_at, stage_signal "
|
f"SELECT id, job_id, subject, received_at, stage_signal, body, from_addr "
|
||||||
f"FROM job_contacts "
|
f"FROM job_contacts "
|
||||||
f"WHERE job_id IN ({sig_placeholders}) "
|
f"WHERE job_id IN ({sig_placeholders}) "
|
||||||
f" AND suggestion_dismissed = 0 "
|
f" AND suggestion_dismissed = 0 "
|
||||||
|
|
@ -321,6 +321,8 @@ def list_interviews():
|
||||||
"subject": sr["subject"],
|
"subject": sr["subject"],
|
||||||
"received_at": sr["received_at"],
|
"received_at": sr["received_at"],
|
||||||
"stage_signal": sr["stage_signal"],
|
"stage_signal": sr["stage_signal"],
|
||||||
|
"body": sr["body"],
|
||||||
|
"from_addr": sr["from_addr"],
|
||||||
})
|
})
|
||||||
|
|
||||||
db.close()
|
db.close()
|
||||||
|
|
@ -384,6 +386,34 @@ def dismiss_signal(signal_id: int):
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
# ── POST /api/stage-signals/{id}/reclassify ──────────────────────────────
|
||||||
|
|
||||||
|
VALID_SIGNAL_LABELS = {
|
||||||
|
'interview_scheduled', 'offer_received', 'rejected',
|
||||||
|
'positive_response', 'survey_received', 'neutral',
|
||||||
|
'event_rescheduled', 'unrelated', 'digest',
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReclassifyBody(BaseModel):
|
||||||
|
stage_signal: str
|
||||||
|
|
||||||
|
@app.post("/api/stage-signals/{signal_id}/reclassify")
|
||||||
|
def reclassify_signal(signal_id: int, body: ReclassifyBody):
|
||||||
|
if body.stage_signal not in VALID_SIGNAL_LABELS:
|
||||||
|
raise HTTPException(400, f"Invalid label: {body.stage_signal}")
|
||||||
|
db = _get_db()
|
||||||
|
result = db.execute(
|
||||||
|
"UPDATE job_contacts SET stage_signal = ? WHERE id = ?",
|
||||||
|
(body.stage_signal, signal_id),
|
||||||
|
)
|
||||||
|
rowcount = result.rowcount
|
||||||
|
db.commit()
|
||||||
|
db.close()
|
||||||
|
if rowcount == 0:
|
||||||
|
raise HTTPException(404, "Signal not found")
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
# ── POST /api/jobs/{id}/move ───────────────────────────────────────────────────
|
# ── POST /api/jobs/{id}/move ───────────────────────────────────────────────────
|
||||||
|
|
||||||
STATUS_TIMESTAMP_COL = {
|
STATUS_TIMESTAMP_COL = {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,9 @@ def tmp_db(tmp_path):
|
||||||
subject TEXT,
|
subject TEXT,
|
||||||
received_at TEXT,
|
received_at TEXT,
|
||||||
stage_signal TEXT,
|
stage_signal TEXT,
|
||||||
suggestion_dismissed INTEGER DEFAULT 0
|
suggestion_dismissed INTEGER DEFAULT 0,
|
||||||
|
body TEXT,
|
||||||
|
from_addr TEXT
|
||||||
);
|
);
|
||||||
CREATE TABLE background_tasks (
|
CREATE TABLE background_tasks (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
|
|
@ -73,6 +75,8 @@ def test_interviews_includes_stage_signals(client):
|
||||||
assert signals[0]["stage_signal"] == "interview_scheduled"
|
assert signals[0]["stage_signal"] == "interview_scheduled"
|
||||||
assert signals[0]["subject"] == "Interview confirmed"
|
assert signals[0]["subject"] == "Interview confirmed"
|
||||||
assert signals[0]["id"] == 10
|
assert signals[0]["id"] == 10
|
||||||
|
assert "body" in signals[0]
|
||||||
|
assert "from_addr" in signals[0]
|
||||||
|
|
||||||
# neutral signal excluded
|
# neutral signal excluded
|
||||||
signal_types = [s["stage_signal"] for s in signals]
|
signal_types = [s["stage_signal"] for s in signals]
|
||||||
|
|
@ -157,3 +161,42 @@ def test_dismiss_signal_sets_flag(client, tmp_db):
|
||||||
def test_dismiss_signal_404_for_missing_id(client):
|
def test_dismiss_signal_404_for_missing_id(client):
|
||||||
resp = client.post("/api/stage-signals/9999/dismiss")
|
resp = client.post("/api/stage-signals/9999/dismiss")
|
||||||
assert resp.status_code == 404
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
# ── Body/from_addr in signal response ─────────────────────────────────────
|
||||||
|
|
||||||
|
def test_interviews_signal_includes_body_and_from_addr(client):
|
||||||
|
resp = client.get("/api/interviews")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
jobs = {j["id"]: j for j in resp.json()}
|
||||||
|
sig = jobs[1]["stage_signals"][0]
|
||||||
|
# Fields must exist (may be None when DB column is NULL)
|
||||||
|
assert "body" in sig
|
||||||
|
assert "from_addr" in sig
|
||||||
|
|
||||||
|
|
||||||
|
# ── POST /api/stage-signals/{id}/reclassify ────────────────────────────────
|
||||||
|
|
||||||
|
def test_reclassify_signal_updates_label(client, tmp_db):
|
||||||
|
resp = client.post("/api/stage-signals/10/reclassify",
|
||||||
|
json={"stage_signal": "positive_response"})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json() == {"ok": True}
|
||||||
|
con = sqlite3.connect(tmp_db)
|
||||||
|
row = con.execute(
|
||||||
|
"SELECT stage_signal FROM job_contacts WHERE id = 10"
|
||||||
|
).fetchone()
|
||||||
|
con.close()
|
||||||
|
assert row[0] == "positive_response"
|
||||||
|
|
||||||
|
|
||||||
|
def test_reclassify_signal_invalid_label(client):
|
||||||
|
resp = client.post("/api/stage-signals/10/reclassify",
|
||||||
|
json={"stage_signal": "not_a_real_label"})
|
||||||
|
assert resp.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_reclassify_signal_404_for_missing_id(client):
|
||||||
|
resp = client.post("/api/stage-signals/9999/reclassify",
|
||||||
|
json={"stage_signal": "neutral"})
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue